Windows-商店应用开发入门指南-六-

139 阅读1小时+

Windows 商店应用开发入门指南(六)

原文:Beginning Windows Store Application Development – HTML and JavaScript Edition

协议:CC BY-NC-SA 4.0

二十、打印

我写软件已经很多年了。每当讨论添加打印功能的话题时,我都会有两种内部反应。如果我在做一个 Web 开发项目,我会想,“好吧,没什么大不了的。易于打印的网页通常很简单。”如果我正在开发一个 WinForms 或 WPF 桌面应用,我会想,“唉。”给桌面应用添加打印支持并不是什么难事,但通常比其他具有类似价值的功能更具挑战性。

幸运的是,如果您正在使用 HTML 和 JavaScript 构建 Windows Store 应用,那么向已安装的应用添加打印支持也很简单。正如您将在本章中看到的,打印可以使用您当前用于打印网页的相同技术来完成。但是,Windows 应用商店应用还提供了额外的打印功能,只需多做一点工作,就可以获得一些其他方式无法获得的额外优势。

这件事的美妙之处在于你可以选择。如果额外的好处(我将在接下来的几页中讨论)不值得在您的应用中额外增加 50 行左右的代码,那么您不必这样做。当然,我即将给你那 50 行代码,所以也许这是值得的。

网页开发-风格打印

如果你的背景是 web 开发,那么我将在本章中讨论的从 Windows 应用商店打印的第一个技术应该是熟悉的。在这一节中,我将演示使用 CSS 和window.print在 Clok 中打印项目细节屏幕,这是一个内置于 HTML 浏览器(如 Internet Explorer)中的功能,它与 Windows Store 应用共享相同的 HTML 呈现引擎。我将在本节中介绍以下内容:

  • 将打印按钮添加到项目详细信息屏幕的应用栏
  • 使用媒体查询来指定仅在打印时应用的 CSS 规则
  • 实现将内容发送到打印机的代码

向项目详细信息屏幕添加打印按钮

我们要做的第一步是向项目详细信息屏幕的应用栏添加一个打印按钮。将清单 20-1 中的代码添加到pages\projects\detail.htmlAppBar定义的末尾。

清单 20-1。 向应用栏添加打印按钮

<button
    data-win-control="WinJS.UI.AppBarCommand"
    data-win-options="{
        id:'printCommand',
        label:'Print',
        icon:'url(/img/Print-small-sprites.png)',
        section:'global',
        tooltip:'Print',
        disabled: true}">
</button>

image 本书附带的源代码包括一个完整的项目,其中包含本章使用的所有源代码和图像文件。您可以在本书的产品详细信息页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790)。

与方向和时间表应用栏按钮类似,当应用处于快照视图时,这个打印按钮也应该隐藏。将清单 20-2 中突出显示的代码添加到媒体查询中的detail.css中,该查询指定了 Clok 何时处于快照视图中的规则。

清单 20-2。 在抓拍视图中隐藏打印按钮

#projectDetailAppBar #goToDirectionsCommand,
#projectDetailAppBar #goToTimeEntriesCommand,
#projectDetailAppBar #printCommand {
    display: none;
}

此外,只有当用户查看现有项目的详细信息时,才应启用此按钮。将清单 20-3 中的代码添加到detail.js中的configureAppBar函数中。

清单 20-3。 启用打印按钮

printCommand.winControl.disabled = false;

CSS 媒体查询用于打印

下一步是指定打印时将应用的替代 CSS 规则。与项目详细信息屏幕的当前布局相比(见图 20-1 ),该屏幕的打印版本不需要后退按钮或如此宽的左边缘边距。此外,应用栏和当前时间应该隐藏。最后,Description 字段应该扩展以显示更长的文本,为了更好地衡量,我们将在打印时将其移动到页面的末尾。

9781430257790_Fig20-01.jpg

图 20-1 。项目详细信息屏幕

该表单使用 CSS 网格布局在屏幕上定位各种元素。在第十一章中,添加了项目细节屏幕,我们在detail.html中用内联样式指定了-ms-grid-row-ms-grid-column CSS 属性。因为您将在打印时使用 CSS 来重新定位描述字段,所以您必须对定义它的 HTML 做一点小小的更改。用清单 20-4 中突出显示的代码更新detail.html

清单 20-4。 更新描述表单字段

<div class="formField" id="descriptionLabelAndField"
        style="-ms-grid-column: 1; -ms-grid-column-span: 3;" >
    <label for="projectDescription">Description</label><br />
    <textarea id="projectDescription" data-win-bind="value: description"></textarea>
</div>

CSS 属性-ms-grid-row已经被删除,现在必须在新规则中将其添加到detail.css中。将清单 20-5 中突出显示的代码添加到detail.css中。

清单 20-5。 在屏幕上查看时,将描述字段定位在网格的第二行

#descriptionLabelAndField {
    -ms-grid-row: 2;
}

#projectDescription {
    height: 60px;
    width: calc(90vw - 120px);
}

现在,您必须定义打印屏幕时将应用的 CSS 规则。如果您有许多规则,您可以将它们添加到第二个 CSS 文件中,并从detail.html中引用它们。在这种情况下,不需要太多规则,所以将来自清单 20-6 的 CSS 添加到detail.css的末尾。

清单 20-6。 CSS 媒体查询打印

@media print {
    .fragment header[role=banner] {
        -ms-grid-columns: 0px 1fr;
    }

    .detail section[role=main] {
        margin-left: 0px;
        margin-right: 0px;
    }

    .detail header .win-backbutton {
        display: none;
    }

    #projectDetailForm .formField.required input,
    #projectDetailForm .formField.required textarea,
    #projectDetailForm .formField.required select {
        border: inherit;
        background-color: inherit;
    }

    #descriptionLabelAndField {
        -ms-grid-row: 7;
    }

    #projectDescription {
        height: 350px;
        width: 90vw;
    }

    #currentTime,
    #projectDetailAppBar {
        display: none;
    }
}

因为这些规则包含在打印介质查询(@media print)中,所以它们将仅在打印该屏幕时应用。这些规则实现了我在本节开始时描述的各种需求,比如删除 Back 按钮,将 Description 字段放在 CSS 网格布局的第七行,从而将它移动到页面的末尾。

发送到打印机

最后一步是将页面发送到打印机。将清单 20-7 中的代码添加到detail.js中。还要确保在detail.jsready函数中连接这个click事件处理程序。

清单 20-7。 打印屏幕

printCommand_click: function (e) {
    window.print();
},

立即运行 Clok 并导航到现有项目的项目详细信息屏幕。如果点击打印按钮,打印窗格将会打开,允许您从安装在您计算机上的打印机列表中选择一台打印机(参见图 20-2 )。

9781430257790_Fig20-02.jpg

图 20-2 。打印机选择

选择打印机后——本例中为 Microsoft XPS Document Writer 显示打印预览窗格(参见图 20-3 )。此视图允许您查看将要打印内容的缩略图,以及所选打印机支持的一些打印选项。在图 20-3 中,唯一可见的选项是将方向从纵向改为横向。

9781430257790_Fig20-03.jpg

图 20-3 。打印预览

image 注意微软 XPS Document Writer 是一款虚拟打印机,可以将任何可以打印的内容转换成 XPS 文件。如果我选择了物理打印机,我会看到其他选项,如更改打印份数或将彩色模式从彩色更改为黑白的选项。可以为打印预览窗格配置附加选项。有关这些其他选项的更多信息,请访问 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh761453.aspx)。

在打印预览窗格中,单击打印按钮会将文档提交到选定的打印机。在图 20-2 和图 20-3 的例子中,我的电脑上会创建一个 XPS 文件。

WinRT 打印

在上一节中,我描述了用于打印网页的典型方法:定义打印友好的 CSS 规则并启动打印过程。当构建 web 应用时,你可以通过调用window.print来触发打印,类似于你在清单 20-7 中看到的。此外,网络浏览器本身提供打印功能来执行相同的任务。默认情况下,Windows Store 应用没有打印功能,但是 Windows 允许应用使用 Devices charm 进行打印(参见图 20-4 )。

9781430257790_Fig20-04.jpg

图 20-4 。Windows 设备魅力

与 Windows Share charm 一样,默认情况下不会向其他设备发送任何内容(参见图 20-5 )。然而,通过添加相对少量的代码,您可以使您的应用发送到其他设备,如打印机。

9781430257790_Fig20-05.jpg

图 20-5 。此应用目前无法发送到其他设备

创建打印机类

这种与 Windows 的集成是在应用中使用 WinRT 打印类的好处之一。您可以在应用中包含一个打印按钮来打印某些内容,但是在应用中添加必要的挂钩来支持 Windows 打印界面是一个简单的步骤,可以使您的应用看起来更加完美。

在应用中声明打印契约比添加搜索契约或共享目标契约更简单。你不必对package.appxmanifest做任何修改。在最简单的层面上,需要三个步骤。

  1. 获取一个Windows.Graphics.Printing.PrintManager类的实例并处理它的printtaskrequested事件。
  2. 创建一个Windows.Graphics.Printing.PrintTask类的实例。
  3. 指定要打印的源文档。

虽然这三个步骤是打印的唯一要求,但在打印时,您还可以采取其他一些步骤来使您的应用更加健壮和完善。例如,应用中支持打印的每个屏幕都必须处理PrintManager对象的printtaskrequested事件。但是,在任何给定时间,只能有一个此事件的处理程序处于活动状态。因此,如果您有多个必须支持打印的屏幕,则必须注销与当前可见屏幕不关联的此事件的处理程序。

此外,PrintTask对象有一个completed事件,您可以选择处理它来通知您的应用打印过程是成功还是失败。使用 WinRT 打印类的另一个主要优点是,从 Windows 向应用反馈打印作业的状态。使用window.print时,您的应用不会收到任何关于打印作业状态的反馈。在许多情况下,这不是必需的,但是如果成功的打印对于应用的用户来说是至关重要的一步,那么使用 WinRT 打印类获得的附加信息将会很有帮助。

在这一节中,我将讨论我添加到 Clok 中的一个类,该类封装了这些细节,并使向应用中任意数量的屏幕添加打印支持变得更加容易。在 Visual Studio 项目的js文件夹中创建一个名为printing.js的新 JavaScript 文件。请务必在default.html中添加对该文件的引用。添加从清单 20-8 到printing.js的代码。

清单 20-8。 添加打印实用程序类

(function () {
    "use strict";

    var printingClass = WinJS.Class.define(
        function ctor() {
            this.printManager = Windows.Graphics.Printing.PrintManager.getForCurrentView();
            this.printManager_printtaskrequested_boundThis
                = this.printManager_printtaskrequested.bind(this);
            this._document = null;
            this._title = "Clok";
            this._completed = null;
        },
        {
            register: function (title, completed) {
                this._title = title || this._title;
                this._completed = completed || this._completed;
                this.printManager.addEventListener("printtaskrequested",
                    this.printManager_printtaskrequested_boundThis);
            },

            unregister: function () {
                this.printManager.removeEventListener("printtaskrequested",
                    this.printManager_printtaskrequested_boundThis);
            },

            setDocument: function (doc) {
                this._document = doc;
            },

            print: function () {
                Windows.Graphics.Printing.PrintManager.showPrintUIAsync();
            },

            printManager_printtaskrequested: function (e) {
                if (this._document) {
                    var printTask = e.request.createPrintTask(this._title, function (args) {
                        args.setSource(MSApp.getHtmlPrintDocumentSource(this._document));
                        printTask.oncompleted = this._completed;
                    }.bind(this));
                }
            },
        }
    );

    WinJS.Namespace.define("Clok", {
        Printer: printingClass,
    });
})();

在这段代码定义的Clok.Printer类的构造函数中,获得了当前视图的PrintManager。在register函数中,它的printtaskrequested事件被处理,在unregister函数中,该处理程序被移除。print功能只需打开 Windows 打印界面,点击设备图标即可打开。printManager_printtaskrequested处理函数创建所需的PrintTask对象,表明要打印的源是在setDocument函数中指定的值。如果在register函数中指定了completed事件处理程序,当PrintTask对象的completed事件被引发时,它将被调用。

发送到打印机

上一节中创建的Printer类封装了支持应用中最常见的打印场景所需的所有逻辑。在这一节中,我将向您展示如何在支持打印的屏幕上使用Printer类。我们将更新本章前面添加到项目细节屏幕的打印逻辑,以使用新的Printer类。

image 注意本章前面添加到项目细节屏幕的打印友好的 CSS 仍然需要用于这个部分。使用 WinRT 打印类不会影响文档的打印呈现方式。它只影响如何将呈现的文档发送到打印机。

在本章的前面,我指定了只有在查看现有项目时才应该启用打印。使用window.print技术时,用户将永远无法使用设备的魅力启动打印过程。在这种情况下,只需禁用新项目的打印按钮就足够了。在更新 Clok 以使用 WinRT 打印类之后,用户将能够从 Devices charm 进行打印,因此禁用 Clok 中的打印按钮是不够的。相反,您必须指定应该打印的文档。将清单 20-9 中的函数添加到detail.js中。

清单 20-9。 指定要打印的文件

configurePrintDocument: function (existingId) {
    if (existingId) {
        this.printer.setDocument(document);
    } else {
        this.printer.setDocument(null);
    }
},

在这种情况下,当查看一个现有的项目时,当前的document对象——如果您是 web 开发人员,您所熟悉的同一个document对象——将被发送到打印机。如果用户没有查看现有项目,则指定null,禁用当前屏幕的打印。在这一章的后面,我将讨论你必须打印替代内容的一些选项,也就是说,你如何打印不仅样式不同于当前屏幕而且实际上完全不同的内容。

configurePrintDocument定义引用了this.printer,您还没有定义它。将清单 20-10 中高亮显示的代码添加到detail.js中的ready函数和unload函数中。

清单 20-10。 注册和注销打印机类的实例

ready: function (element, options) {
    this.printer = new Clok.Printer();
    this.printer.register("Project Detail", function (e) {
        if (e.completion === Windows.Graphics.Printing.PrintTaskCompletion.failed) {
            // printing failed
        }
    });

    this.setCurrentProject(options);

    this.configureAppBar(options && options.id);
    this.configurePrintDocument(options && options.id);
    var form = document.getElementById("projectDetailForm");
    WinJS.Binding.processAll(form, this.currProject);

    // SNIPPED
},

unload: function () {
    app.sessionState.currProject = null;
    app.removeEventListener("checkpoint", this.app_checkpoint_boundThis);
    this.printer.unregister();
},

ready中,this.printer被定义,它的register函数被调用。在这个例子中,我已经为completed事件指定了一个处理程序,但是我把实现留给了您,作为一个练习。目前,我只添加了一个打印任务失败的条件。此外,您可以测试e.completion何时为submittedcanceledabandoned。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.graphics.printing.printtaskcompletion)上有关于Windows.Graphics.Printing.PrintTaskCompletion枚举的文档。

除了为completed事件指定一个处理程序,我还为底层的PrintTask对象指定了一个标题。在许多情况下,用户永远不会看到这个标题,但是有两种情况下,一个友好的、相关的标题会对用户有所帮助。

  • They will see this title when they view the printer’s queued jobs (see Figure 20-6).

    9781430257790_Fig20-06.jpg

    图 20-6 。打印机队列为古腾堡,我家的打印机

  • 当使用 XPS Document Writer 或其他创建文件而不是将文档发送到物理打印机的虚拟打印机驱动程序时,它将是默认文件名。

还有一个步骤来改变项目细节屏幕,以使用 WinRT 打印类来代替window.print。在我们的新Printer类中,对window.print的调用必须替换为对print函数的调用。用清单 20-11 中的代码更新detail.js中的printCommand_click处理函数的定义。

清单 20-11。 用新的打印机类打印

printCommand_click: function (e) {
    this.printer.print();
},

对于最简单的打印任务,window.print可能就足够了。然而,正如您在本节中看到的,与 Windows 集成用于打印需要少量的开发投资。在许多应用中,在清单 20-8 中定义的Clok.Printer类只需稍加修改或不加修改就可以使用,所以我鼓励你在必须打印的应用中加入这个功能。

打印替代内容

前两节演示了打印当前屏幕的打印机友好版本。然而,有时你必须打印不同但相关的内容。有时,由 WinJS 控件生成的 HTML 不适合打印,即使它包含您希望打印的内容。在这一节中,我将展示两种可以用来解决打印替代内容问题的技术:

  • head元素中指定打印的替代文档
  • iframe打印文件

为了演示这些技术,我们将让用户能够打印驾驶路线和发票。在这两种情况下,我们都将使用在上一节中创建的Clok.Printer类。

打印驾驶路线

在规划本章内容时,我想到的第一个可能的打印示例是从方向屏幕打印驾驶方向。事实证明,从ListView开始打印并不总是能得到想要的结果。我没有尝试为方向屏幕上的ListView控件生成的复杂 HTML 制定易于打印的 CSS 规则,而是决定直接从 Bing 地图网站打印方向。

使用 HTML 页面的head中的link元素,您可以指定打印时应该使用的替代内容。清单 20-12 展示了一个在link元素中使用媒体查询来指定打印替代内容的例子。

清单 20-12。 使用链接元素

<link id="alternateContent" rel="alternate" media="print" href="printer.html" />

Bing 地图打印友好页面的链接是动态的。显示从密尔沃基到华盛顿州雷德蒙方向的页面非常适合打印


http://www.bing.com/maps/print.aspx?cp=45.3601835,-105.018913&pt=pf&rtp=pos.43.041809_-87.906837_Milwaukee%2C%20WI∼pos.47.678558_-122.130989_Redmond%2C%20WA

因为它是动态的,所以您不能将link元素添加到directions.html中,如清单 20-12 所示。相反,您将构建这个link元素,并在directions.js中将它动态添加到页面中。在本节的其余部分,我将带您了解 Clok 中需要进行的更改,以允许用户从 Bing 地图网站打印行驶方向。

向方向屏幕添加打印按钮

我们要做的第一步是向方向屏幕添加一个带有打印按钮的应用栏。将清单 20-13 中的代码添加到pages\projects\directions.html中。

清单 20-13。 向路线屏幕添加应用栏

<div id="directionsAppBar"
    class="win-ui-dark"
    data-win-control="WinJS.UI.AppBar"
    data-win-options="{ sticky: true }">

    <button
        data-win-control="WinJS.UI.AppBarCommand"
        data-win-options="{
            id:'printCommand',
            label:'Print',
            icon:'url(/img/Print-small-sprites.png)',
            section:'global',
            tooltip:'Print',
            disabled: true}">
    </button>
</div>

虽然方向屏幕不会打印用户正在查看的文档,但是您仍然可以使用您在本章前面添加的Clok.Printer类来打印 Bing 地图中的行驶方向。在下一节中,我们将对Printer类做一些小的修改,以便更容易地支持为打印指定替代内容。同时,用清单 20-14 中突出显示的代码更新directions.js

清单 20-14。 注册、注销、处理 Printer 类实例的点击事件

ready: function (element, options) {
    this.printer = new Clok.Printer();
    this.printer.register("Directions");

    printCommand.onclick = this.printCommand_click.bind(this);

    // SNIPPED
},

unload: function () {
    // SNIPPED

    this.printer.unregister();
},

printCommand_click: function (e) {
    this.printer.print();
},

image 注意诚然,Clok 的方向屏幕不像 Bing 地图网站那样全面,没有地图或改变目的地的手段。为了给用户提供这些选项,你会发现,在本书附带的源代码中,我在应用栏中添加了一个额外的按钮,用于启动用户当前搜索的 Bing 地图网站。您可以在本书的产品详细信息页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790)。

更改打印机类别

打印替代内容的任务可以在不对本章前面添加的Printer类做任何修改的情况下完成。必须添加到页面的head元素中的link元素可以很容易地在directions.js中创建。事实上,我最初就是这样开发这个功能的。然而,本着使Printer类更容易重用的精神,在这一节中,我将带您经历一些小的变化,以将该逻辑添加到Printer类中。用清单 20-15 中突出显示的代码更新printing.js

清单 20-15。 支持打印机类内的替换内容

unregister: function () {
    this.printManager.removeEventListener("printtaskrequested",
        this.printManager_printtaskrequested_boundThis);
    this._removeAlternateContent();
},

setAlternateContent: function (href) {
    this._removeAlternateContent();

    var alternateContent = document.createElement("link");
    alternateContent.setAttribute("id", "alternateContent");
    alternateContent.setAttribute("rel", "alternate");
    alternateContent.setAttribute("href", href);
    alternateContent.setAttribute("media", "print");
    document.getElementsByTagName("head")[0].appendChild(alternateContent);

    this.setDocument(document);
},

_removeAlternateContent: function () {
    var alternateContent = document.getElementById("alternateContent");
    if (alternateContent) {
        document.getElementsByTagName("head")[0].removeChild(alternateContent);
    }
},

setAlternateContent函数中创建了link元素。在创建并添加到document对象后,setDocument被当前的document对象调用,该对象包含必要的link元素作为参数。在定义link元素之前,当Printer类被取消注册时,调用_removeAlternateContent函数,该函数用于删除任何先前指定的替代内容。这种安全措施确保了永远不会指定一个以上的这样的元素。

更改 BingMaps 类

易于打印的 Bing 地图页面的 URI 包括经度和纬度坐标,如下所示:


http://www.bing.com/maps/print.aspx?cp=45.3601835,-105.018913&pt=pf&rtp=pos.43.041809_-87.906837_Milwaukee%2C%20WI∼pos.47.678558_-122.130989_Redmond%2C%20WA

需要起点和终点的坐标,以及它们之间中心点的坐标。目前,Clok.Data.BingMaps类不包含任何坐标,但是因为 Bing Maps API 包含了所需的坐标,所以添加这些值会很容易。用清单 20-16 中突出显示的代码更新data文件夹中bingMapsWrapper.jsgetDirections函数。

清单 20-16。 包括起点和终点的坐标

var directions = {
    copyright: resp.copyright,
    distanceUnit: resp.resourceSets[0].resources[0].distanceUnit,
    durationUnit: resp.resourceSets[0].resources[0].durationUnit,
    travelDistance: resp.resourceSets[0].resources[0].travelDistance,
    travelDuration: resp.resourceSets[0].resources[0].travelDuration,
    bbox: resp.resourceSets[0].resources[0].bbox ,
    startCoords: resp.resourceSets[0].resources[0].routeLegs[0].actualStart.coordinates,
    endCoords: resp.resourceSets[0].resources[0].routeLegs[0].actualEnd.coordinates
}

image 注意如果你正在使用本书附带的源代码,在运行这些示例之前,你必须在bingMapsWrapper.js中添加你的 Bing 地图 API 密钥。

设置替代内容

使用户能够打印驾驶路线指引的最后一步是指定应该打印的替代内容。因为替代内容是由 URI 指定的,所以当用户请求方向时,必须构建该 URI。用清单 20-17 中突出显示的代码更新directions.js中的getDirectionsButton_click处理函数。

清单 20-17。 将 URI 构造为打印友好页面,并将其设置为要打印的替代内容

getDirectionsButton_click: function (e) {
    printCommand.winControl.disabled = true;
    this.printer.setDocument(null);

    if (fromLocation && fromLocation.value && this.dest) {

        maps.getDirections(fromLocation.value, this.dest)
            .then(function (directions) {

                if (directions
                        && directions.itineraryItems
                        && directions.itineraryItems.length > 0) {

                    WinJS.Binding.processAll(
                        document.getElementById("directionsContainer"), directions);

                    this.showDirectionResults(true);

                    directionsListView.winControl.itemDataSource
                        = directions.itineraryItems.dataSource;

                    directionsListView.winControl.forceLayout();

                    var printPage = " [`www.bing.com/maps/print.aspx?cp`](http://www.bing.com/maps/print.aspx?cp) ="
                        + ((directions.startCoords[0] + directions.endCoords[0]) / 2) + ","
                        + ((directions.startCoords[1] + directions.endCoords[1]) / 2)
                        + " & pt=pf & rtp=pos." + directions.startCoords[0] + "_"
                        + directions.startCoords[1] + "_" + fromLocation.value
                        + "∼pos." + directions.endCoords[0] + "_" + directions.endCoords[1]
                        + "_" + this.dest

                    this.printer.setAlternateContent(printPage);

                    printCommand.winControl.disabled = false;
                } else {
                    this.showDirectionResults(false);
                }
            }.bind(this), function (errorEvent) {
                this.showDirectionResults(false);
            }.bind(this));
    } else {
        this.showDirectionResults(false);
    }
},

该功能的第一个变化是当用户发起新的驾驶路线指引请求时禁用打印功能。调用setDocument函数是通过null调用的,因为该参数将禁用当前屏幕的打印,即使用户试图使用设备的魅力进行打印。如果用户请求的驾驶方向可用,则创建 URI 并传递给setAlternateContent函数。这将启用从设备的魅力打印,但不会自动启用打印应用栏按钮,所以我们明确启用它。

立即运行 Clok,导航到指定了客户地址的项目的方向屏幕,然后输入起始位置并单击获取方向。方向载入后,使用打印按钮或 Devices charm 打印行驶方向(参见图 20-7 )。

9781430257790_Fig20-07.jpg

图 20-7 。便于打印的驾驶路线预览

image 如果您使用 Microsoft XPS Document Writer 测试您的打印功能,它会在您的 Documents 文件夹中保存一份 XPS 文档。

打印项目时间表的发票

在上一节中,您使用了一个link元素来指定打印的替代内容。该内容的 URI 可以引用应用中的一个文件或 Internet 上的一个页面,但无论是哪种情况,打印的内容都是通过请求指定 URI 的内容来检索的。在这一节中,我将讨论在一个iframe元素中托管内容,并指定iframe的内容应该用作打印的替代内容,我将向您展示如何使用这种技术从时间表屏幕构建一个可打印的发票。

添加发票选项设置弹出按钮

到目前为止,在 Clok 中,用户还没有办法指定他们自己公司的名称或他们的计费率——这是创建发票所需的两件事。在本节中,您将添加一个新的发票选项设置弹出按钮,以允许用户指定这两个值,并提供一个段落来描述他们的账单条款。这些值可以添加到您在第十五章中创建的现有 Clok 选项设置弹出按钮的新部分,但是,在我看来,它们与该设置弹出按钮上可用的其他设置无关,所以我建议添加一个新的。因为这些步骤与构建“Clok 选项设置”弹出按钮的步骤几乎相同,所以我不会在这里讨论细节,但我会突出显示您需要采取的步骤。

  1. 在 Visual Studio 项目的settings文件夹中创建一个名为invoiceOptions.html的新设置弹出按钮和一个名为invoiceOptions.js的对应 JavaScript 文件。

  2. 允许用户为三个新的漫游设置提供值:

    • a.invoiceCompanyName
    • b.invoiceDefaultRate
    • c.invoicePaymentOptions
  3. Specify default values for these roaming settings in the intializeRoamingSettings function in default.js (see Listing 20-18). These values will be used if the user does not specify values of his or her own.

    清单 20-18。 为新的漫游设置提供默认值

    roamingSettings.values["invoiceCompanyName"] =
        roamingSettings.values["invoiceCompanyName"] || "Your Company Name";
    
    roamingSettings.values["invoiceDefaultRate"] =
        roamingSettings.values["invoiceDefaultRate"] || 50.00;
    
    roamingSettings.values["invoicePaymentOptions"] =
        roamingSettings.values["invoicePaymentOptions"] || "Payment is due within 30 days.";
    
  4. 在设置了applicationcommands变量的default.js中引用这个新的设置弹出按钮,使其包含在设置窗格中。

该过程与您在第十五章中添加锁定选项设置弹出按钮时遵循的过程相同。本书随附的源代码中提供了发票选项设置弹出按钮的完整版本,以及本章中的所有其他源代码。

image 注意这些变化将允许用户指定一个单一的计费率用于所有项目。您可能希望添加的一个有用功能是允许用户另外指定每个项目的计费率。如果指定了项目费率,则该费率将用于发票计算;否则,将使用默认汇率。

更新时间表屏幕

在 HTML ( pages\timeSheets\list.html)和 JavaScript 代码(pages\timeSheets\list.js))中,需要对时间表屏幕进行一些更改。按照本章前面几节中使用的相同模式,在时间表屏幕的应用栏中添加一个打印发票按钮。不要忘记注册一个Clok.Printer的实例,并通过调用Printer实例的print函数来处理打印发票按钮的click事件。

在本节中,您将使用一个iframe元素的内容作为打印的替代内容。现在将那个iframe元素添加到list.html(参见清单 20-19)。

***清单 20-19。***将包含发票的 iframe 元素

<iframe height="0" width="0" id="invoiceFrame" src="/templates/invoice.html"></iframe>

iframe可以放在屏幕的任何地方,因为它没有高度和宽度。但是,我建议将它放在timeEntriesContainerdiv之前,并将heightwidth属性临时设置为正数,这样您就可以在测试期间看到iframe的内容。src属性中引用的invoice.html文件尚不存在。我将在下一节介绍该文件的细节,但同时,在 Visual Studio 项目的根目录下创建一个名为templates的新文件夹,然后在该文件夹中添加一个名为invoice.html的占位符 HTML 文件(参见图 20-8 )。

9781430257790_Fig20-08.jpg

图 20-8 。向 Visual Studio 项目添加发票模板

仅当使用应用栏中的过滤器按钮选择单个项目时,才能打印发票。当用户更新过滤器时,您必须添加代码来确定项目是否被选中。用清单 20-20 中突出显示的代码更新list.js中的filter_changed函数。

清单 20-20。 过滤器更换时重新生成发票

filter_changed: function (e) {
    this.updateResultsArea(searchInProgress);
    this.printer.setDocument(null);
    printInvoiceCommand.winControl.disabled = true;

    storage.timeEntries.getSortedFilteredTimeEntriesAsync(
            this.filter.startDate,
            this.filter.endDate,
            this.filter.projectId)
        .then(
            function complete(results) {
                if (results.length <= 0) {
                    timeEntryAppBar.winControl.show();
                    this.updateResultsArea(noMatchesFound);
                } else {
                    if (ClokUtilities.Guid.isGuid(this.filter.projectId)) {
                        this.printer.setDocument(invoiceFrame.document);
                        printInvoiceCommand.winControl.disabled = false;
                    }
                    this.updateInvoiceIframe(results);
                    this.updateResultsArea(timeEntriesListView);
                }
                this.showAddForm();
                this.filteredResults = results;
                timeEntriesListView.winControl.itemDataSource = results.dataSource;
            }.bind(this),
            function error(results) {
                this.updateResultsArea(searchError);
            }.bind(this)
        );
},

当一个项目被选中,并且this.filter.projectId包含一个 GUID 时,调用setDocument函数,将iframedocument对象作为参数。接下来您将添加的updateInvoiceIframe函数用于将时间表数据从时间表屏幕传递到iframe中的发票。将清单 20-21 中的代码从添加到list.js

清单 20-21。 发送时间单数据到发票

updateInvoiceIframe: function (results) {
    var invoiceLines = results.map(function (item) {
        return {
            elapsedSeconds: item.elapsedSeconds,
            dateWorked: item.dateWorked,
            notes: item.notes
        };
    });

    var invoiceProject = results.getAt(0).project;

    var invoiceData = {
        project: invoiceProject,
        lines: invoiceLines
    }
    invoiceFrame.postMessage(invoiceData, "ms-appx://" + document.location.host);
},

iframe的交流是使用postMessage完成的,与在第十三章中完成的与 Web 工作器的交流方式非常相似。创建一个invoiceData对象,它包含发票上要包含的每个时间条目的详细信息,以及对正在开票的项目的引用。出于安全原因,对postMessage的调用包括当前域作为第二个参数。在invoice.html,我们将验证发布的消息来自同一个域。

生成发票

在本节中,我将展示invoice.html的内容,当用户从时间表屏幕打印时,将打印该文件。iframe可以包含您希望应用包含的任何内容。对于 Clok,我将invoice.html构建为一个单独的文件,在文件中包含必要的 CSS 和 JavaScript,而不是像我们在应用的其余部分所做的那样引用外部特定于页面的 CSS 和 JavaScript 文件。在上一节中,您创建了一个占位符invoice.html文件。用清单 20-22 中的代码更新文件。如果您不想键入所有代码,您可以在本书附带的源代码中找到该文件的完整版本。

***清单 20-22。***invoice.html 发票模板的内容

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style type="text/css">
        body {
            font-family: sans-serif;
        }

        h4.sectionHead {
            margin-bottom: 0px;
        }

        .invoiceLines {
            border-collapse: collapse;
            border-spacing: 0px;
        }

            .invoiceLines th,
            .invoiceLines td {
                border: 1px solid black;
                margin: 0px;
                padding: 2px;
            }

            .invoiceLines .totals {
                font-weight: bold;
            }

            .invoiceLines #totalDesc {
                background: black;
            }
    </style>

    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <script src="/data/timeEntry.js"></script>
    <script src="/data/project.js"></script>

    <script>
        var appData = Windows.Storage.ApplicationData.current;
        var roamingSettings = appData.roamingSettings;

        var formatDate = function (dt) {
            var formatting = Windows.Globalization.DateTimeFormatting;
            var formatter = new formatting.DateTimeFormatter("shortdate");
            return formatter.format(dt);
        }

        window.onmessage = function (message) {
            if (message.origin !== "ms-appx://" + document.location.host) {
                return;
            }

            WinJS.UI.processAll().then(function () {
                var compName = roamingSettings.values["invoiceCompanyName"];
                var rate = Number(roamingSettings.values["invoiceDefaultRate"]);
                var pmtOptions = roamingSettings.values["invoicePaymentOptions"];
                if (pmtOptions.indexOf("<br") < 0) {
                    pmtOptions = pmtOptions.replace(/\r\n/g, "<br />").replace(/\n/g, "<br />");
                }

                var invoiceLines = document.getElementById("invoiceLines");
                var template = document.getElementById("invoiceLineTemplate").winControl;

                var sumHour = 0;
                var sumCost = 0;

                invoiceLines.innerText = "";
                message.data.lines.forEach(function (item) {
                    var hrs = item.elapsedSeconds / 3600;

                    item.dateWorked = formatDate(item.dateWorked);
                    item.hours = hrs.toFixed(2);
                    item.lineCost = (rate * hrs).toFixed(2);

                    sumHour += hrs;
                    sumCost += rate * hrs;

                    template.render(item, invoiceLines);
                });

                invoiceDate.innerText = formatDate(new Date());
                projectName.innerText = message.data.project.name;
                projectNumber.innerText = message.data.project.projectNumber;

                companyName.innerText = compName;
                clientName.innerHTML = message.data.project.clientName;
                contactName.innerHTML = message.data.project.contactName;
                address1.innerHTML = message.data.project.address1;
                address2.innerHTML = message.data.project.address2;
                city.innerHTML = message.data.project.city;
                region.innerHTML = message.data.project.region;
                postalCode.innerHTML = message.data.project.postalCode;

                totalHours.innerText = sumHour.toFixed(2);
                totalCost.innerText = sumCost.toFixed(2);

                paymentOptions.innerHTML = pmtOptions;
            });
        }

    </script>
</head>
<body>
    <h1>Invoice</h1>
    <h2 id="companyName"></h2>

    <h4 class="sectionHead">To:</h4>
    <div>
        <div id="clientName"></div>
        <div id="contactName"></div>
        <div id="address1"></div>
        <div id="address2"></div>
        <div>
            <span id="city"></span>,
            <span id="region"></span>
            <span id="postalCode"></span>
        </div>
    </div>

    <h4 class="sectionHead">For:</h4>
    <div>
        <div>
            Invoice Date: <span id="invoiceDate"></span>
            <br />
            Project:  <span id="projectName"></span>
            <br />
            Ref #:  <span id="projectNumber"></span>
            <br />
        </div>
    </div>

    <h4 class="sectionHead">Invoice Details</h4>
    <table style="display: none;">
        <tbody data-win-control="WinJS.Binding.Template" id="invoiceLineTemplate">
            <tr>
                <td data-win-bind="textContent: dateWorked"></td>
                <td data-win-bind="textContent: notes"></td>
                <td data-win-bind="textContent: hours"></td>
                <td data-win-bind="textContent: lineCost"></td>
            </tr>
        </tbody>
    </table>

    <table class="invoiceLines">
        <thead>
            <tr>
                <th>Date</th>
                <th>Note</th>
                <th>Hours</th>
                <th></th>
            </tr>
        </thead>
        <tbody id="invoiceLines"></tbody>
        <tfoot>
            <tr class="totals">
                <td id="totalDesc" colspan="2"></td>
                <td id="totalHours"></td>
                <td id="totalCost"></td>
            </tr>
        </tfoot>
    </table>

    <p id="paymentOptions"></p>
</body>
</html>

invoice.html大部分都是标准的、无趣的 HTML、CSS、JavaScript。然而,我在清单 20-22 中强调了几件事,我想指出来。

  • 因为invoice.html加载了ms-appx协议,它可以完全访问 WinRT 和 WinJS 库,以及我们添加到 Clok 中的任何类。你可以在上面看到,我已经添加了对 WinJS JavaScript 文件以及 Clok 的ProjectTimeEntry类定义的脚本引用。
  • 作为引用 WinJS 库的结果,我能够利用WinJS.Binding.Template类创建一个名为invoiceLineTemplate的模板,该模板定义了发票中的行项目将如何显示。
  • onmessage处理函数中,通过清单 20-21 中的postMessage发送的invoiceData可作为message.data使用。基于该对象的属性填充单个占位符,发票的行项目由invoiceLineTemplate模板呈现。
  • 您在本章前面创建的三个漫游设置的值用于显示用户的公司名称和帐单条款,以及计算应付金额。

立即运行 Clok 并导航至时间表屏幕。在过滤了单个项目的列表后,您可以使用应用栏中的打印发票按钮或使用 Devices charm 来打印发票(参见图 20-9 )。

9781430257790_Fig20-09.jpg

图 20-9 。从时间表屏幕打印的发票样本

高级打印主题

在本章中,我只讲述了从 Windows 应用商店打印的基础知识。当然,打印遵循 80/20 规则,即你需要完成的 80%的工作是用 20%的可能特性完成的。甚至可能是 90/10 法则。然而,Windows.Graphics.Printing名称空间中还有许多其他的类,您可能会发现它们对一些更特殊的打印需求很有用。例如,有一些类可以简化任务,比如交流打印任务的进度,或者允许用户在打印预览窗格中指定更多或更少的选项,比如打印质量或纸张大小。

MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.graphics.printing.aspx)上有关于Windows.Graphics.Printing名称空间的更多信息。

结论

从 Windows 应用商店应用打印的基础与从网站打印非常相似。在这一章中,我介绍了一些定义打印内容的不同方法,以及一些启动打印过程的不同方法。可以通过对屏幕内容应用仅打印 CSS 规则、链接到应该打印而不是当前屏幕的替代内容的 URI,或者打印iframe元素的内容来指定内容。您可以使用原生的window.print函数来启动打印过程,但是只需少量的工作,您就可以利用 WinRT 打印类,为您的用户提供更加集成的体验。只需稍加修改或不加修改,本章创建的Clok.Printer类应该能够处理您可能面临的大量打印任务。

二十一、通知和磁贴

在我看来,Windows 8 最好的一个特点是,只要看一眼屏幕,就能获得如此多的信息。

随着 Live Tiles 的引入,应用可以以小块的形式向用户提供最新的信息。在许多情况下,我只需要摘要信息(“你的下一次会议是明天早上在约翰的办公室”或“现在气温 78 度,阳光明媚”),而其他时候,它会提示我启动应用以获得更多细节(“约翰刚刚通过电子邮件向我发送了我们会议的议程”或“我安装的四个应用有可用的更新”)。此外,toast 通知是一个小矩形,在屏幕的右上角显示一条简短的消息,无论您是在开始屏幕上还是使用其他应用,它都是一种在发生事情时从应用获取更新的好方法。

在这一章中,我将介绍几种不同的方法,让你的用户可以方便地访问你的应用中的信息。我将介绍用于显示小更新的 toast 通知和 Live Tiles,我还将介绍用于让用户快速访问应用中常用屏幕的辅助 Tiles。

吐司通知

Toast 通知,有时简称为toast通知,是从应用向用户提供简短、及时信息的一种很好的方式。这些通知在屏幕的上角显示为一个小矩形,无论用户在计算机上做什么,无论是使用您的应用、使用不同的 Windows 应用商店应用 、在开始屏幕上还是在桌面上,这些通知都会出现(参见图 21-1 )。用户可以单击通知来激活您的应用;他们可以驳回通知;或者他们可以简单地忽略它,它就会消失。

9781430257790_Fig21-01.jpg

图 21-1 。Toast 通知,同时使用商店应用,提醒我写这一章

有几种类型的通知可用:

  • Local :当用户使用您的应用时创建并显示的通知
  • Scheduled :在用户使用你的应用时创建的通知,但是直到将来某个特定的时间才会显示,那时用户可能在使用你的应用,也可能不在使用
  • Push :从远程服务器(如 Windows Azure Mobile Services)创建并发送的通知,显示用户是否正在使用你的应用

本地通知

在这一节中,我将向您展示如何向 Clok 添加一个简单的 toast 通知。当用户在定时器已经运行的情况下恢复时钟时,将显示通知,告知用户定时器已经运行了多长时间。

对应用清单的更改

在您的应用可以显示任何 toast 通知之前,您必须在应用清单中做一个小的配置更改。这是一个很容易被忽视的简单步骤。如果忽略此步骤,则不会出现错误;创建通知的代码只是被悄悄地忽略了。在我意识到我的错误之前,我曾经浪费了大约十分钟试图找出为什么通知没有被显示。

幸运的是,这是一个简单的改变。打开package.appxmanifest并向下滚动到应用 UI 选项卡的可视资产部分。从左侧列表中选择所有图像资产,并将 Toast capable 设置为 Yes(见图 21-2 )。

9781430257790_Fig21-02.jpg

图 21-2 。启用 toast 通知

此外,Windows 模拟器的一个限制是它不显示 toast 通知。在测试该功能之前,确保您的调试目标设置为本地机器或远程机器(见图 21-3 )。

9781430257790_Fig21-03.jpg

图 21-3 。更改调试目标

Toast 通知模板

当决定向用户显示通知时,您必须仔细考虑必须包含哪些信息。根据你的需要,你可以从 WinRT 库中的八个模板中选择一个(见图 21-4 )。

9781430257790_Fig21-04.jpg

图 21-4 。WinRT 中可用的 toast 通知模板

所有八个图标都包括在package.appxmanifest中为您的应用指定的小图标。其中四个模板允许您指定要包含的文本—根据您选择的模板,在一至三段之间。例如,如果您需要显示一个简短的句子,您可以选择toastText01模板,而toastText02模板将允许您指定一个标题,以及另一段将换行的文本。此外,其他四个模板提供了相同的选择,但也允许您指定通知文本附带的图像。各种模板记录在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh761494.aspx)上。

image 注意虽然每段文本没有特定的字符限制,但如果某个字符串对于其分配的空间来说太长,它会被截断,并添加省略号。记住这一点,确保你展示的每一条信息都清晰简洁。

创建通知

让我们通过向 Clok 添加通知来看看这是如何工作的。要显示这八个模板中的一个,您必须创建 XML 来定义要使用的模板和要在通知中显示的值。您将更新 Clok 仪表板以显示通知 Clok 已经启动,计时器已经运行。将清单 21-1 中定义的函数添加到home.js

清单 21-1。 创建通知

getStillRunningToastContent: function () {
    var seconds = elapsedTimeClock.winControl.timerValue;

    if (elapsedTimeClock.winControl.isRunning && seconds > 0) {
        var hours = Math.floor(Clok.Utilities.SecondsToHours(seconds, false));

        var template = notifications.ToastTemplateType.toastImageAndText02;
        var toastContent = notificationManager.getTemplateContent(template);

        // image
        var imageNodes = toastContent.getElementsByTagName("image");
        imageNodes[0].setAttribute("src", "ms-appx:///img/Clock-Running.png");

        // text
        var textNodes = toastContent.getElementsByTagName("text");
        textNodes[0].appendChild(toastContent.createTextNode("Clok is running"));
        textNodes[1].appendChild(toastContent.createTextNode(
"Clok has been running for more than " + hours + " hours."));

        return toastContent;
    }
},

showLocalToast: function () {
    var toastContent = this.getStillRunningToastContent();

    if (toastContent) {
        var toast = new notifications.ToastNotification(toastContent);
        notificationManager.createToastNotifier().show(toast);
    }
},

getStillRunningToastContent功能首先确定定时器是否正在运行以及已经过了多长时间。如果用户刚刚启动了 Clok,并且计时器仍在运行,则创建一个通知。在本例中,我选择了toastImageAndText02模板,它允许我在通知中指定一个图像、一个标题和一段稍长的文本。

在清单 21-1 中,我使用了getTemplateContent函数来检索一个Windows.Data.Xml.Dom.XmlDocument对象,该对象表示创建通知所需的 XML。然后我操纵那个XmlDocument对象的节点来指定应该显示的图像和文本。除了操作一个XmlDocument对象,您还可以构建一个包含必要 XML 的字符串,并从中创建一个通知。创建动态切片的过程遵循相同的步骤,我将在本章后面的“创建动态切片”一节中演示字符串操作技术。

``getStillRunningToastContent函数返回通知,showLocalToast函数将通知传递给ToastNotifier对象的show函数,后者将显示通知。现在将清单 21-2 中突出显示的代码添加到home.js中的setDashboardStateFromSettings`函数中。

清单 21-2。 显示通知

setDashboardStateFromSettings: function () {
    var state = localSettings.values["dashboardState"];

    if (state) {
        state = JSON.parse(state);

        elapsedTimeClock.winControl.startStops = state.startStops;
        project.selectedIndex = this.getIndexOfProjectId(state.projectId);
        timeNotes.value = state.timeNotes;

        if (elapsedTimeClock.winControl.isRunning) {
            this.startTimer();
            this.showLocalToast();
        }
    }
},

此外,将清单 21-3 中突出显示的别名添加到home.js

清单 21-3。 给通知类添加别名

var appData = Windows.Storage.ApplicationData.current;
var localSettings = appData.localSettings;
var notifications = Windows.UI.Notifications;
var notificationManager = notifications.ToastNotificationManager;

var nav = WinJS.Navigation;
var storage = Clok.Data.Storage;

现在运行 Clok 并启动计时器,然后关闭 Clok。片刻之后,再次启动 Clok。此时,Clok 会显示一个通知,提醒你定时器正在运行(见图 21-5 )。

9781430257790_Fig21-05.jpg

图 21-5 。向 Clok 用户显示通知

虽然我不会在这里介绍它,但这本书附带的源代码包括一个简单的功能,可以改善用户体验,允许用户在 Clok 选项设置弹出按钮中指定他们是否希望在 Clok 启动时看到这个提醒。您可以在本书的产品详细信息页面(www.apress.com/9781430257790)的源代码/下载选项卡上找到本章的代码示例。

预定通知

在上一节中,您添加了一些功能,让用户在启动 Clok 时清楚地知道计时器是否已经在运行。作为一名用户,我可能会发现这很有用,例如,当我启动 Clok 来获取到我的客户所在位置的驾驶路线时,我会想起我昨天启动了计时器。如果我已经完成了这项工作,这个提醒可能会促使我快速停止计时器,并在我记忆犹新的时候更正时间条目。

然而,当我不使用 Clok 时,情况会怎样呢?如果我正在写电子邮件或玩游戏呢?在这一节中,我将向您展示如何安排在计时器运行八小时后出现通知。

安排通知

在上一节中,您创建了一个ToastNotification对象,并使用show函数将它立即显示给用户。安排通知非常类似。在本节中,您将创建一个ScheduledToastNotification对象,并使用addToSchedule函数在未来的某个时间将它显示给用户。将清单 21-4 中的代码添加到home.js中。

清单 21-4。 调度未来通知

scheduleToast: function () {
    var reminderThreshold = 8; // hours
    var toastContent = this.getStillRunningToastContent();

    if (toastContent) {
        var seconds = elapsedTimeClock.winControl.timerValue;
        var notifyTime = (new Date()).addSeconds(-seconds).addHours(reminderThreshold);
        if (notifyTime.getTime() > (new Date()).getTime()) {
            var snoozeTime = 30 * 60 * 1000; // 30 min
            var snoozeCount = 5;
            var toast = new notifications.ScheduledToastNotification(
                toastContent,
                notifyTime,
                snoozeTime,
                snoozeCount);
            toast.id = "IsRunningToast";
            notificationManager.createToastNotifier().addToSchedule(toast);
        }
    }
},

image 注意在本书附带的源代码中,我给Date原型添加了几个新函数:addSecondsaddMinutesaddHours

这段代码使用相同的getStillRunningToastContent函数来定义通知。在做了一些日期和时间计算以确定计时器何时到达八小时后,从getStillRunningToastContent函数返回的toastContent对象被用来创建一个ScheduledToastNotification对象。此外,我还添加了可选代码,允许用户将通知“暂停”30 分钟,最多五次。可以通过忽略通知、在触摸屏上将其扫走或点击鼠标悬停在通知上时出现的×按钮来暂停通知。

如果用户点击通知,打盹将被取消;将推出 Clok 并且将显示 Clok 仪表板屏幕。如果您为定义通知的XmlDocument中的toast节点的launch属性指定一个值,那么您可以检查args.detail.arguments属性以在应用的激活过程中检索该值,并导航到不同的屏幕,而不是 Clok 仪表板屏幕。例如,如果您正在创建日历应用,单击会议提醒通知应该会在应用中打开该会议。关于launch属性的更多信息可以在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/br230846.aspx)上找到。在本章的后面,当我介绍二级瓷砖时,我将涉及一个不同但相似的主题。

在调用addToSchedule之前,我给toast对象的id属性赋值。该id属性最长可达 16 个字符,可用于引用尚未显示的预定通知。此外,如果您使用相同的id属性值创建另一个计划通知,新通知将替换先前定义的通知。通过为每个通知提供不同的id属性值,可以安排多个不同的通知。例如,如果您正在构建一个日历应用,您可以通过为每个通知指定不同的id值,为用户日历中的每个会议安排提醒通知。因为 Clok 中只有一个计时器,所以我将id属性硬编码为IsRunningToast。在下一节中,您将使用它来取消预定的通知。在取消通知之前,必须首先对其进行计划。用清单 21-5 中突出显示的代码更新home.js中的setupTimerRelatedControls函数,以调用新的scheduleToast函数。

清单 21-5。 定时器启动时的预定通知

setupTimerRelatedControls: function () {
    if (this.timerIsRunning) {
        this.startTimer();
        this.scheduleToast();
    } else {
        this.stopTimer();
    }
    this.enableOrDisableButtons();
},

现在,当计时器启动时,如果计时器已经运行了八个小时,将会出现一个通知。如果用户启动 Clok 时计时器已经在运行,他们看到的通知将与他们让计时器运行八小时时看到的通知相同。该行为允许您在创建预定通知时重用现有的getStillRunningToastContent函数。然而,如果你现在运行 Clok 并启动计时器,当通知最终在预定时间出现时,就不太对了(见图 21-6 )。

9781430257790_Fig21-06.jpg

图 21-6 。计时器运行八小时后出现的通知

image 注意为了更快的测试,我建议暂时将notifyTime变量改为(new Date()).addSeconds(20)。当您完成这一部分的测试时,请确保将该值改回清单 21-4 中指定的值,以防止无休止的预定通知循环。

发生了什么事?安排通知在将来显示时,您必须在安排时创建通知的内容。如果我们保持getStillRunningToastContent功能不变,即使计时器运行 8 小时后通知会正确显示,消息也会错误地显示运行时间减少。显示的确切值将取决于用户是否停止和恢复计时器。例如,如果他或她在中午停止计时器吃午饭,然后在午饭后恢复计时器,则消息可能指示计时器已经运行了四个多小时。要解决这个问题,需要做一些改变,以确定最终显示的消息应该是什么。我们必须添加将变量seconds指定为getStillRunningToastContent函数的参数的能力。用清单 21-6 中突出显示的代码更新home.js中的getStillRunningToastContent函数。

清单 21-6。 指定秒为参数

getStillRunningToastContent: function ( seconds ) {
    seconds = seconds || elapsedTimeClock.winControl.timerValue;

    // SNIPPED
},

如果没有为seconds参数提供值,它将遵循您之前在清单 21-1 中添加的逻辑,使用定时器的当前值。因此,您不必对showLocalToast函数做任何修改。然而,必须更新scheduleToast函数以将该值传递给getStillRunningToastContent函数。用清单 21-7 中突出显示的代码更新home.js中的scheduleToast函数。

清单 21-7。 更新到 scheduleToast 功能

scheduleToast: function () {
    var reminderThreshold = 8; // hours
    var toastContent = this.getStillRunningToastContent(reminderThreshold * 60 * 60);

    // SNIPPED
},

现在,当显示预定通知时,它显示正确的信息(见图 21-7 )。

9781430257790_Fig21-07.jpg

图 21-7 。计时器运行八小时后出现的正确通知

image 注意您可能希望添加一个很好的特性来改善用户体验,那就是在 Clok Options settings 弹出菜单中包含一个设置,允许用户为reminderThreshold变量提供一个值。

取消预定通知

您在上一节中所做的更改将安排在计时器运行八小时后出现通知。正确嗯,从技术上来说,它会安排在运行八小时时显示一个通知。我表述的方式略有不同,但这是一个重要的区别。如果用户将计时器停在 7 小时,现在会发生什么?如果他或她保存了时间条目会发生什么?如果他或她丢弃它会发生什么?

如果这一部分的标题还没有给出答案,就目前的情况来看,当计时器到达八小时时,通知仍然会显示。幸运的是,解决方法很简单。每当用户计时器停止时,任何预定的通知都应被取消。将清单 21-8 中高亮显示的代码添加到home.js中的setupTimerRelatedControls函数中。

清单 21-8。 定时器停止时取消预定通知

setupTimerRelatedControls: function () {
    if (this.timerIsRunning) {
        this.startTimer();
        this.scheduleToast();
    } else {
        this.stopTimer();
        this.unscheduleToast();
    }
    this.enableOrDisableButtons();
},

接下来,将清单 21-9 中定义的unscheduleToast函数添加到home.js中。

清单 21-9。 从日程中删除通知

unscheduleToast: function () {
    var notifier = notificationManager.createToastNotifier();
    var scheduled = notifier.getScheduledToastNotifications();

    for (var i = 0, len = scheduled.length; i < len; i++) {
        if (scheduled[i].id === "IsRunningToast") {
            notifier.removeFromSchedule(scheduled[i]);
        }
    }
},

如您所料,getScheduledToastNotifications函数获得了当前为您的应用安排的所有通知的列表。遍历它们,我已经识别出了与我们在清单 21-4 中设置的id值相同的那个,并把它传递给removeFromSchedule函数来取消它。现在,任何时候计时器停止,任何未来的通知都将被取消。

添加声音

在某些情况下,通知可能非常重要,足以抓住用户的注意力。假设您正在构建一个闹钟应用。如果用户睡着了,仅仅显示一个通知不足以引起他或她的注意。或者通知可能是针对用户已经指示的非常重要和及时的事情。例如,如果用户正在打电话而不是在使用计算机,您可以做些什么来提高用户看到通知的可能性?

在这种情况下,您可以考虑在通知中添加声音。将清单 21-10 中高亮显示的代码添加到home.js中的getStillRunningToastContent函数中。

清单 21-10。 包括带通知的音频

getStillRunningToastContent: function (seconds) {
    seconds = seconds || elapsedTimeClock.winControl.timerValue;

    if (elapsedTimeClock.winControl.isRunning && seconds > 0) {

    // SNIPPED

        // audio
        var toastNode = toastContent.selectSingleNode("/toast");
        toastNode.setAttribute("duration", "long");

        var audio = toastContent.createElement("audio");
        audio.setAttribute("src", "ms-winsoundevent:Notification.Looping.Call");
        audio.setAttribute("loop", "true");

        toastNode.appendChild(audio);

        return toastContent;
    }
},

这些更改会导致通知显示更长时间,并在显示时重复播放特定的声音(循环播放)。这是一个简单的改变,将对我们在 Clok 中创建的所有与定时器相关的通知生效。但是,要记住这个特性有一些限制。

您只能引用短列表中的声音。有五种非循环声音适用于简单的通知。事实上,根据你在电脑上的声音控制面板(见图 21-8 )中的设置,你可能已经听到了每个通知的默认声音。我一会儿会回到这一点。

9781430257790_Fig21-08.jpg

图 21-8 。声音控制面板的声音选项卡

此外,还有 20 种更长的声音适合循环播放。这些声音适用于在聊天应用中接收来电的情况,类似于您的电话会响铃几次,让您有机会接听来电。这些循环声音,其中一个是我在清单 21-10 中使用的,不管声音控制面板中的设置如何,都将被播放,即使选择了无声音声音方案。

前五个非循环声音映射到图 21-8 中的程序事件列表中的特定项目。例如,Notification.Default声音对应于声音控制面板中的通知程序事件。如果用户将他或她的声音方案更改为没有为通知事件指定声音的方案,如果您将Notification.Default指定为要播放的声音,他或她将听不到任何声音。这最初给我造成了一段时间的困惑,因为我通常选择无声的声音方案。因此,当我指定一个循环声音时,我会听到声音,但当我指定一个非循环声音时,我听不到声音。

这又引出了另一点。我刚才提到,你可能一直听到每个通知的声音。默认情况下,除非另有说明,否则所有通知在显示时都会播放Notification.Default声音。如果您在计算机上选择了一种声音方案,并且该方案为通知程序事件指定了一种声音,则本章中显示的每个通知都会播放该声音。如果需要,您可以显示没有声音的通知,而不管用户选择的声音方案。为此,不是设置 toast 的 XML 定义的audio节点的src属性,而是将其silent属性设置为true。有关可指定的各种声音的更多信息可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx)上找到。

但是,在为通知设置声音时,以及在一般情况下使用通知时,都要小心。如果用户觉得你的应用太吵,他或她可能会对你的应用感到失望。如果您觉得应该在通知中包含声音,尤其是较长的循环声音,那么允许用户指定他或她想要听到的声音(如果有的话)可能是个好主意。

推送通知

到目前为止,我已经介绍了本地通知和预定通知。在这两种情况下,通知都是在应用运行时创建的。另一方面,推送通知是从另一个服务器创建和发送的。例如,如果您要扩展 Clok 以支持云中具有集中存储系统的多个用户,那么当另一个用户向项目的文档库添加文档时,您可以向一个用户发送推送通知。

正如我在第十四章中提到的,微软的 Windows Azure 移动服务提供了推送通知功能,此外还有许多其他功能,所有这些都在多个平台上得到支持。除了我在第十四章 ( www.windowsazure.com/en-us/develop/mobile)中提到的 Windows Azure Mobile Services 开发中心之外,还有一个将来自 Azure Mobile Services 的推送通知集成到 Windows Store 应用中的示例应用(http://code.msdn.microsoft.com/windowsapps/Tile-Toast-and-Badge-Push-90ee6ff1)。与我在整本书中提到的许多其他示例应用不同,这个示例项目在 Windows SDK 示例应用包中不可用,必须单独下载。

瓷砖

安装应用时,每个 Windows 应用商店应用都有一个添加到开始屏幕的磁贴。这是构建应用的一个要求。事实上,当你从第四章中讨论的任何 Visual Studio 项目模板中创建一个项目时,Visual Studio 会自动向你的项目添加一些默认图像,包括一个名为logo.png的文件,它显示在你的应用的磁贴上(参见图 21-9 )。

9781430257790_Fig21-09.jpg

图 21-9 。每个 Visual Studio 项目都包含默认磁贴徽标

在第二十三章中,我将向你展示如何更新你的应用的磁贴图像和颜色,以补充我们已经在应用中构建的内容。然而,在本章中,我将介绍为您的应用添加宽切片、实时切片和辅助切片。

宽瓷砖

顾名思义,宽瓷砖比标准方形瓷砖宽。在 Windows 8 中,标准磁贴尺寸为 150×150,宽磁贴尺寸为 310×150(参见图 21-10 )。

9781430257790_Fig21-10.jpg

图 21-10 。天气应用的宽瓦

添加宽瓷砖是一个简单的过程。第一步是创建一个图像。对于 Clok,我复制了一份logo.png文件,将其命名为widelogo.png,并将其添加到 Clok Visual Studio 项目的images文件夹中。然后我用一个图像编辑程序将图像文件的宽度增加到 310 像素。第二步,也是最后一步,是在package.appxmanifest中引用该文件(见图 21-11 )。

9781430257790_Fig21-11.jpg

图 21-11 。在应用清单中设置宽平铺

如果您在应用清单中指定了一个宽磁贴,当您的应用安装后,开始屏幕上显示的默认磁贴将是宽磁贴(参见图 21-12 )。用户将能够通过在开始屏幕上右键单击标准图块和宽图块,并从出现的应用栏中选择他或她想要的选项来在标准图块和宽图块之间切换。

9781430257790_Fig21-12.jpg

图 21-12 。宽砖

实时瓷砖

任何使用 Windows 8 超过五分钟的人都熟悉实时磁贴,即使他们不熟悉这个术语。更新开始屏幕上的磁贴以显示相关信息的技术是操作系统的一个流行特性。在这一节中,我将向您展示如何在开始屏幕上为 Clok 创建一个动态磁贴。与本章前面添加的通知类似,如果 Clok 计时器正在运行,实时磁贴会向用户一目了然地显示出来。

平铺模板

在向您展示如何创建动态切片之前,我想快速讨论一下可用于创建动态切片的模板。正如我在本章前面介绍的 toast 通知一样,有许多预定义的模板可以用来创建动态磁贴。虽然有八个模板可用于通知,但有四十六个模板可用于实时平铺,十个用于标准方形平铺,三十六个用于宽平铺。这太多了,本书无法详细介绍。一般来说,它们可以分为以下几种类型的实时切片:

  • Text-only templates: Similar to notifications, these have several formats, each supporting a different number of text elements, such as the example in Figure 21-13. These are available for both standard and wide Live Tiles.

    9781430257790_Fig21-13.jpg

    图 21-13 。纯文本宽模板

  • Image-only templates: As the name suggests, no text elements are specified as part of the template (see Figure 21-14). However, if you have a particular tile layout that cannot be achieved using any of the other templates, one option is to use an image-only template and specify an application-generated image as the content. These are available for both standard and wide Live Tiles.

    9781430257790_Fig21-14.jpg

    图 21-14 。仅宽图像模板

  • Text-and-image templates: These combine images and text into a single Live Tile, as shown in Figure 21-15, and are available only for wide Live Tiles.

    9781430257790_Fig21-15.jpg

    图 21-15 。宽文本和图像模板

  • Peek 图块模板 :这些是实时图块,可以在纯图像视图和类似于其他图块模板的视图之间来回“翻转”。图 21-16 显示了 peek 瓷砖的变化过程。

9781430257790_Fig21-16.jpg

图 21-16 。变化时宽 peek 模板的进展

需要记住的一点是,只有当用户开始屏幕上的方块设置为标准方块大小时,才会使用方块活动方块模板。同样,只有当用户开始屏幕上的图块设置为宽尺寸时,才会使用宽实时图块模板。因此,为应用支持的每个切片大小指定模板是一个很好的做法。我将在下一节向您展示如何做到这一点。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh761491.aspx)上提供了这些类别中所有可用模板的完整列表。

创建实时图块

创建实时互动程序与创建通知非常相似。动态切片模板用于创建指定各种属性的 XML 文档。从上一节提到的模板列表中,我选择了一个标准的正方形模板(TileSquarePeekImageAndText04)和一个宽模板(TileWidePeekImage06)。在我向您展示如何实现这些之前,请将清单 21-11 中突出显示的别名添加到home.js

清单 21-11。 添加另一个别名

var appData = Windows.Storage.ApplicationData.current;
var localSettings = appData.localSettings;
var notifications = Windows.UI.Notifications;
var notificationManager = notifications.ToastNotificationManager;
var tileUpdateManager = notifications.TileUpdateManager;

var nav = WinJS.Navigation;
var storage = Clok.Data.Storage;

接下来,将清单 21-12 中定义的两个函数添加到home.js中。

清单 21-12。 功能启用和禁用实时平铺

enableLiveTile: function () {
    var tileContentString = "<tile>"
        + "<visual>"
        + "<binding template=\"TileSquarePeekImageAndText04\" branding=\"logo\">"
        + "<image id=\"1\" src=\"ms-appx:///img/Clock-Running.png\"/>"
        + "<text id=\"1\">Clok is running</text>"
        + "</binding>  "
        + "<binding template=\"TileWidePeekImage06\" branding=\"none\">"
        + "<image id=\"1\" src=\"ms-appx:///img/widelogo.png\"/>"
        + "<image id=\"2\" src=\"ms-appx:///img/Clock-Running.png\"/>"
        + "<text id=\"1\">Clok is running</text>"
        + "</binding>"
        + "</visual>"
        + "</tile>";

    var tileContentXml = new Windows.Data.Xml.Dom.XmlDocument();
    tileContentXml.loadXml(tileContentString);

    var tile = new notifications.TileNotification(tileContentXml);
    tileUpdateManager.createTileUpdaterForApplication().update(tile);
},

disableLiveTile: function () {
    tileUpdateManager.createTileUpdaterForApplication().clear();
},

image 注意在本章的前面,我展示了如何通过操作XmlDocument对象来创建通知。虽然这也是 Live Tiles 的一个选项,但是在本例中,我构建了一个包含必要 XML 的字符串。无论是创建通知还是动态磁贴,这两种技术都是允许的。

您会注意到,在enableLiveTile函数中,我在同一个 XML 中指定了标准的方形实时图块和宽实时图块,每个图块都在同一个visual元素中包含的不同的binding元素中。当在enableLiveTile函数的末尾调用update函数时,同时指定这两个函数将更新图块的两个版本。如果用户已经将任一尺寸的磁贴固定到他或她的开始屏幕,则静态徽标图像将被适当的动态磁贴替换。唯一剩下的步骤是在计时器运行时启用实时图块,在计时器不运行时禁用它。用清单 21-13 中突出显示的代码更新home.js中的setupTimerRelatedControls函数。

清单 21-13。 定时器启动时启用实时磁贴,定时器停止时禁用

setupTimerRelatedControls: function () {
    if (this.timerIsRunning) {
        this.startTimer();
        this.scheduleToast();
        this.enableLiveTile();
    } else {
        this.stopTimer();
        this.unscheduleToast();
        this.disableLiveTile();
    }
    this.enableOrDisableButtons();
},

现在运行 Clok 并启动计时器。保持计时器运行,并切换到开始屏幕。根据您锁定的瓷砖尺寸,您会看到瓷砖被标准方形活动瓷砖(参见图 21-17 )或宽活动瓷砖(参见图 21-18 )所取代。

9781430257790_Fig21-17.jpg

图 21-17 。克洛克的标准尺寸活瓷砖“偷看”

9781430257790_Fig21-18.jpg

图 21-18 。克洛克的宽尺寸活瓷砖“偷看”

image 注意Windows 模拟器中不显示实时磁贴。为了测试该功能,在测试该功能之前,您必须将调试目标设置为本地机器或远程机器(参见图 21-3)。

次级瓦片

每个 Windows 应用商店应用都自动有一个可以固定在开始屏幕上的磁贴。单击时,它将启动应用并显示应用的默认屏幕。在上一节中,我向您展示了如何将您的应用的默认磁贴更改为动态磁贴,这可以为用户提供一目了然的有用信息。像默认磁贴一样,它也将显示应用的默认屏幕。

这一部分的主题,二级磁贴,也是类似的,它们都可以显示在用户的开始屏幕上。但是,它们是不同的,因为它们会导致应用在启动时加载不同的页面。您可以使用该功能在启动时自动加载应用的顶层部分,例如 Clok 中的时间表屏幕。您还可以使用它来显示更详细的屏幕,例如特定项目的项目详细信息屏幕。在这一节中,我们将在 Clok 中实现后一个功能,允许用户在开始屏幕上添加或删除第二个磁贴——通常称为固定和取消固定——这将允许他们只需单击一下就可以查看 Clok 中特定项目的详细信息。

向项目详细信息屏幕添加按钮

向开始屏幕添加或固定辅助磁贴需要用户的许可。您不能以编程方式将辅助磁贴添加到用户的开始屏幕,因为这是用户单击按钮的直接结果。相反,Windows 提供了一个请求用户许可的界面(见图 21-19 ),当你想要添加磁贴时,你必须显示这个界面,显示在弹出控件中。

9781430257790_Fig21-19.jpg

图 21-19 。请求用户允许将磁贴固定到开始屏幕

在下一节中,我将向您展示如何显示弹出控件来请求用户添加图块的权限。在本节中,您将向项目详细信息屏幕上的应用栏添加一个新按钮。将来自清单 21-14 的代码添加到pages\projects文件夹中detail.html文件的打印按钮之前。

清单 21-14。 添加图钉按钮

<button
    data-win-control="WinJS.UI.AppBarCommand"
    data-win-options="{
        id:'pinUnpinCommand',
        label:'Pin to Start',
        icon:'pin',
        section:'global',
        tooltip:'Pin to Start',
        disabled: true}">
</button>

用户将点击该按钮以将项目固定到他或她的开始屏幕上。如果项目已经固定,此按钮可用于从开始屏幕移除或取消固定。在接下来的几节中,我将向您展示如何重新使用这个按钮来处理锁定和取消锁定任务。在此之前,还有几个剩余的任务需要完成,到本书的这一点应该已经很熟悉了。在detail.jsready函数中,为这个按钮添加一个click事件处理程序到一个名为pinUnpinCommand_click的函数中,这个函数将在下一节中添加。另外,不要忘记更新configureAppBar功能,以便在查看现有项目的详细信息时启用该按钮。

固定辅助单幅图块

在这一节中,我将介绍将辅助磁贴添加到用户开始屏幕所需的几个步骤。首先将清单 21-15 中的的高亮别名添加到detail.js

清单 21-15。 添加别名

var app = WinJS.Application;
var startScreen = Windows.UI.StartScreen;
var secondaryTile = startScreen.SecondaryTile;
var data = Clok.Data;
var storage = Clok.Data.Storage;

当创建第二个图块时,构造函数要求您提供一个tileId参数。该值是最多 64 个字符的字符串,可以包括字母、数字、句点或下划线字符。您将使用该字符串在整个应用中标识图块。在这一章的后面,我将向你展示如何使用这个值智能地处理来自二级磁贴的应用激活。您还将使用该值来允许用户取消固定磁贴。将清单 21-16 中突出显示的代码添加到detail.js中的ready函数中。

清单 21-16。 为图块设置一个 id

ready: function (element, options) {
// SNIPPED

    this.setCurrentProject(options);
    this.secondaryTileId = "Tile.Project." + this.currProject.id.replace(/-/g, ".");

    // SNIPPED
},

使用该值的另一种情况是当使用 WinRT 库来确定用户的开始屏幕上是否已经存在该磁贴时。将清单 21-17 中的 app bar 按钮click事件处理程序添加到detail.js中。

清单 21-17。 处理点击事件

pinUnpinCommand_click: function (e) {
    if (!secondaryTile.exists(this.secondaryTileId)) {
        this.pinToStart();
    }
},

如果图块不存在,则调用pinToStart函数请求用户允许添加图块。将清单 21-18 中的pinToStart函数添加到detail.js中。

清单 21-18。 请求允许将磁贴添加到开始屏幕

pinToStart: function () {
    // build the tile that will be added to the Start screen
    var uriLogo = new Windows.Foundation.Uri("ms-appx:///img/Projects.png");
    var displayName = this.currProject.name + " (" + this.currProject.clientName + ")";

    var tile = new secondaryTile(
        this.secondaryTileId,
        displayName,
        displayName,
        this.currProject.id,
        startScreen.TileOptions.showNameOnLogo,
        uriLogo);

    tile.foregroundText = startScreen.ForegroundText.light;

    // determine where to display the request to the user
    var buttonRect = pinUnpinCommand.getBoundingClientRect();
    var buttonCoordinates = {
        x: buttonRect.left,
        y: buttonRect.top,
        width: buttonRect.width,
        height: buttonRect.height
    };
    var placement = Windows.UI.Popups.Placement.above;

    // make the request and update the app bar
    tile.requestCreateForSelectionAsync(buttonCoordinates, placement)
        .done(function (isCreated) {
            // TODO
        }.bind(this));
},

pinToStart函数中的前几行创建了用户将被要求添加到他或她的开始屏幕上的磁贴。我指定了显示在 Clok Dashboard 屏幕上的相同项目图标作为此标题上的图标。用户可以更改的名称将有一个由项目和客户名称组成的缺省值,指定showNameOnLogo将使这个名称显示在图标下面。除了为tileId参数提供secondaryTileId,我还提供了当前项目的id属性作为arguments参数。在下一节中,我将向您展示如何在 Clok 的激活过程中使用arguments参数。

pinToStart函数的下几行用于确定请求用户许可的弹出按钮将显示在哪里。建议的做法是将弹出按钮的位置基于打开弹出按钮的应用栏按钮的位置。这可以防止不必要的鼠标移动,并使这个过程对用户来说更加自然。pinToStart函数中的最后一段代码请求用户的许可并添加图块。这是异步发生的,我们将向done函数添加更多的代码,以便在过程完成时更新应用栏。

运行 Clok 并导航到现有项目的项目详细信息屏幕。单击“锁定以启动应用栏”按钮,然后在打开的弹出按钮中单击“锁定以启动”按钮(参见前面的图 21-19 )。几秒钟后,一个新的磁贴会出现在你的开始屏幕上(参见图 21-20 )。新的磁贴会添加到开始屏幕的末尾,因此您可能需要滚动才能看到磁贴。

9781430257790_Fig21-20.jpg

图 21-20 。用户添加到他或她的开始屏幕上的第二块磁贴

您并不局限于单个副牌。您的用户可以为他们经常使用的项目添加图块。此外,除了制作静态的次级磁贴,正如我们在本节中所做的,您还可以为您的次级磁贴创建动态磁贴。为辅助切片创建实时切片的过程与为应用本身创建实时切片的过程几乎相同。不像在清单 21-12 中那样调用createTileUpdaterForApplication函数,而是调用createTileUpdaterForSecondaryTile函数,将secondaryTileId作为参数传递,类似于清单 21-19 中的代码。

清单 21-19。 为辅助图块创建活动图块

var secondaryTile = new notifications.TileNotification(secondaryTileXml);
tileUpdateManager.createTileUpdaterForApplication(secondaryTileId).update(secondaryTile);

从辅助图块激活时钟

此时,您可以在开始屏幕上添加辅助磁贴,但是如果您单击此磁贴,您会发现自己回到了 Clok 仪表板上,而不是查看特定项目的项目详细信息屏幕。向default.js中的launchActivation添加几行将纠正这一问题。将清单 21-20 中突出显示的代码添加到default.js中。

清单 21-20。 更新了 launchActivation 功能

if (args.detail.kind === activation.ActivationKind.search) {
    // SNIPPED
} else if ((args.detail.tileId.indexOf("Tile.Project.") >= 0)
        && (ClokUtilities.Guid.isGuid(args.detail.arguments))) {
    nav.navigate("/pages/projects/detail.html", { id: args.detail.arguments });
} else if (nav.location) {
    nav.history.current.initialPlaceholder = true;
    return nav.navigate(nav.location, nav.state);
} else {
    return nav.navigate(Application.navigator.home);
}

args.detail.tileId属性中可以找到被点击的辅助磁贴的tileId属性。尽管在 Clok 中,我们只添加了一种类型的辅助磁贴,我还是决定使用tileId属性来决定如何处理辅助磁贴的激活。如果tileId属性的值是正确的格式,并且如果arguments属性的值是 GUID,那么应用导航到所选项目的项目细节屏幕。

image 注意当您的应用启动时,您也可以通过点击一个 toast 通知来使用arguments属性。在这种情况下,arguments属性的值是在通知的 XML 定义的launch属性中设置的。

取消固定辅助单幅图块

假设一个用户将一个项目的辅助块钉在他或她的开始屏幕上。当项目完成时,他或她可能想要从开始屏幕上移除该图块。当然,这可以直接在开始屏幕上完成,方法是右键单击磁贴并从出现的应用栏中选择 Unpin from Start。在这一节中,我将向您展示允许用户从 Clok 中移除磁贴的步骤。

完成此任务的第一步是更新应用栏中的 Pin to Start 按钮。将清单 21-21 中定义的函数添加到detail.js

清单 21-21。 改变应用栏按钮

updatePinUnpinCommand: function () {
    if (secondaryTile.exists(this.secondaryTileId)) {
        pinUnpinCommand.winControl.icon = "unpin";
        pinUnpinCommand.winControl.label = "Unpin from Start";
        pinUnpinCommand.winControl.tooltip = "Unpin from Start";
    } else {
        pinUnpinCommand.winControl.icon = "pin";
        pinUnpinCommand.winControl.label = "Pin to Start";
        pinUnpinCommand.winControl.tooltip = "Pin to Start";
    }
},

该函数被调用时,将更新应用栏中的pinUnpinCommand按钮。如果图块已经存在,按钮将被更改以指示单击时将取消固定图块。如果图块不存在,按钮将被更新,指示单击它会将图块添加到开始屏幕。现在,我们必须从几个不同的位置调用这个函数。首先,用来自清单 21-22 的高亮代码更新detail.js中的ready函数。

清单 21-22。 调用函数更新 App 栏按钮

ready: function (element, options) {
    // SNIPPED

    this.setCurrentProject(options);
    this.secondaryTileId = "Tile.Project." + this.currProject.id.replace(/-/g, ".");
    this.updatePinUnpinCommand();

    // SNIPPED
},

在清单 21-18 中的函数中,还必须从添加到pinToStart函数中的done函数中调用updatePinUnpinCommand函数。将清单 21-23 中突出显示的代码添加到detail.js中的pinToStart函数中。

清单 21-23。 添加磁贴后调用函数更新应用栏按钮

// make the request and update the app bar
tile.requestCreateForSelectionAsync(buttonCoordinates, placement)
    .done(function (isCreated) {
        this.updatePinUnpinCommand();
    }.bind(this));

现在,当一个项目有一个二级磁贴固定在开始屏幕上时,应用栏图标会变成从开始处取消固定。现在单击该按钮实际上不会做任何事情,因为click事件处理程序只在图块不存在时做一些事情。通过将清单 21-24 中突出显示的代码添加到pinUnpinCommand_click处理函数中来改变这一点。

清单 21-24。 如果图块已经存在,则执行不同的操作

pinUnpinCommand_click: function (e) {
    if (!secondaryTile.exists(this.secondaryTileId)) {
        this.pinToStart();
    } else {
        this.unpinFromStart();
    }
},

与请求向开始屏幕添加新互动程序的权限一样,您还必须请求从开始屏幕移除现有辅助互动程序的权限。unpinFromStart函数为你处理这个问题(参见清单 21-25 )。该功能与pinToStart功能有相似之处。它确定在哪里显示请求,然后异步请求用户删除选定的图块。如果成功,调用updatePinUnpinCommand函数将从开始应用栏按钮切换回开始按钮。

清单 21-25。 请求用户允许从开始屏幕中移除次级磁贴

unpinFromStart: function () {
    var buttonRect = pinUnpinCommand.getBoundingClientRect();
    var buttonCoordinates = {
        x: buttonRect.left,
        y: buttonRect.top,
        width: buttonRect.width,
        height: buttonRect.height
    };
    var placement = Windows.UI.Popups.Placement.above;

    var tile = new secondaryTile(this.secondaryTileId);

    tile.requestDeleteForSelectionAsync(buttonCoordinates, placement)
        .done(function (success) {
            this.updatePinUnpinCommand();
        }.bind(this));
},

现在运行 Clok 并导航到一个项目的项目详细信息屏幕,该项目在开始屏幕上有一个辅助图块。应用栏中的按钮将更改为“从开始处取消固定”。点击此按钮将在弹出控件中显示删除图块的请求(参见图 21-21 )。单击弹出按钮中的“从开始位置取消固定”按钮将从开始屏幕中移除磁贴,并将应用栏按钮切换回默认状态。

9781430257790_Fig21-21.jpg

图 21-21 。请求用户允许从开始屏幕中删除一个互动程序

Windows 的未来版本

在撰写本文时,Windows 下一版本的预览版已经发布。Windows 8.1 将推出两种新的磁贴尺寸,大(310×310)和小(70×70)。此外,将添加新的动态切片模板以支持新的大切片尺寸(小切片将不支持动态切片)。因此,模板的名称将会改变。本章中使用的名称,例如TileSquarePeekImageAndText04,对于 Windows 8 应用仍然是必需的,并且在 Windows 8.1 应用中也将受到支持,但在 Windows 的未来版本中可能会被删除。

作为即将到来的变化的一个例子,在 Windows 8.1 发布后的任何新开发中,当前命名为TileSquarePeekImageAndText04的磁贴模板应该被称为TileSquare150x150PeekImageAndText04。这些以及其他即将到来的变化在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/bg182890.aspx)上都有描述。

结论

随着用户对 Windows 8 越来越熟悉,他们会希望能够快速、轻松地访问信息。通过本章中介绍的简单步骤,您可以在通知和动态磁贴中提供即时信息,并使用辅助磁贴快速访问应用中常用的信息。并不是每个应用都是这些技术的完美候选,不必要的添加这些特性会让你的应用看起来嘈杂或者繁忙。但是,经过仔细考虑,您可以构建一个不仅能满足用户基本需求,还能改善用户体验的应用。`

二十二、摄像机和位置

在撰写本文时,几乎所有的平板电脑和笔记本电脑都内置了网络摄像头。我查看了今天在一个流行的零售网站上列出的前 15 台笔记本电脑,每一台都包括一个网络摄像头。我自己的笔记本电脑有一个内置在盖子里,我的 Surface 平板电脑有两个——一个前置摄像头和一个后置摄像头。对于计算机没有内置网络摄像头的用户来说,USB 网络摄像头是一项相当便宜的投资,一些基本型号的价格不到 20 美元。

在以前的 Windows 版本中,将相机集成到应用中并不总是一个简单的解决方案。然而,在 Windows 8 中,微软创造了一种简单、直接的方式来为您的应用添加照片功能。

除了集成摄像头之外,通过利用来自 Windows 位置提供商的数据,您可以让您的应用具有位置感知能力。WinRT 使用简单的 API 公开位置数据,允许您的应用请求计算机的当前位置或处理事件以在计算机移动时接收更新。

在这一章中,我将向你展示如何将相机和位置数据集成到 Clok 中。Clok 用户将能够部署相机,将照片添加到项目的文档库。他们还将能够获得从当前位置到客户办公室的行车路线。

照相机

你参加过有人在白板上写字的会议吗?也许他们在绘制应用的用户界面,或者绘制流程图,或者只是做笔记。无论如何,当会议结束后,在另一群人进入房间参加他们自己的会议之前,可能会发生一些事情。

  • 有人匆忙地试图将所有的笔记记录到纸上或他或她的电脑上的一个文档中。
  • 有人在白板上用大字写下“保存”,打算在将来的某个时候重新查看笔记或图表。
  • 有人掏出手机,给白板拍了张照片。

我个人用手机拍了很多白板的照片。在本节中,您将向 Clok 添加一个功能,该功能将允许用户使用平板电脑上的相机来捕捉这样的照片,并将它们添加到项目的文档库中。

image 在本章中,我将使用术语网络摄像头摄像头照片视频。在这些例子中,我指的是连接到用户电脑的网络摄像头捕捉到的图像或视频。您将在本章中添加到 Clok 的功能将只对拥有内置网络摄像头或 USB 网络摄像头的用户可用。此外,您的计算机必须有网络摄像头,以便测试您将在此部分添加的功能。在这本书里,我不会讨论使用数码相机,比如傻瓜相机或者 DSLR 相机。

虽然已经可以使用相机应用拍摄照片,然后使用 Share charm 将其添加到文档库中,但您在本节中所做的更改将允许您从 Clok 应用中捕捉照片,而不必单独启动相机应用。在应用中使用网络摄像头有两种方法。你可以使用CameraCaptureUI类或者MediaCapture类。

CameraCaptureUI 类

CameraCaptureUI类允许你用相对较少的代码行,快速地为你的应用添加相机功能。摄像头捕捉功能由内置的 Windows 界面处理,在概念上与文件打开选择器非常相似。在这一节中,我将解释如何向您的用户展示这个界面,以及如何将他们拍摄的照片放入文档库中。

应用清单更改

访问连接到用户计算机的 webam 是一个潜在的安全问题。因此,您必须在项目的应用清单中指明它可能会使用用户的网络摄像头。当一个潜在用户在 Windows Store 中阅读你的应用时,这个事实就被公之于众了(见图 22-1 )。

9781430257790_Fig22-01.jpg

图 22-1 。Windows 应用商店中的相机应用列表

对清单的更改是一个简单的复选框。打开package.appxmanifest并切换到功能选项卡。在功能列表中,勾选网络摄像头项目(参见图 22-2 )。

9781430257790_Fig22-02.jpg

图 22-2 。指定网络摄像头功能

然而,即使在应用清单中有这个声明,您的应用也不能完全开放对摄像机的访问。你的应用第一次试图访问摄像机时——你将在接下来的章节中添加代码来完成——用户被提示确认他或她将允许它(见图 22-3 )。

9781430257790_Fig22-03.jpg

图 22-3 。提示用户获得许可

此外,用户可以随时更改此设置,方法是打开权限设置弹出按钮并切换网络摄像头设置的值(参见图 22-4 )。

9781430257790_Fig22-04.jpg

图 22-4 。用户可以随时撤销该权限

当您使用CameraCaptureUI类时,窗口将显示的界面将提示用户更改权限,如果他或她以前阻止了对摄像机的访问(见图 22-5 )。

9781430257790_Fig22-05.jpg

图 22-5 。CameraCaptureUI 界面指示用户启用权限来使用相机

更新 Clok 仪表板屏幕上的摄像头按钮

指定了摄像头功能,下一步是给用户一个访问摄像头屏幕的方法,我们将在下一节中构建。Clok 仪表盘屏幕上的相机菜单选项已经存在,但目前尚未实现。从home.html中移除清单 22-1 中高亮显示的notImplemented CSS 类。

清单 22-1。 移除高亮显示的 CSS 类

<div id="cameraMenuItem" class="mainMenuItem secondaryMenuItem notImplemented ">

您还必须在home.js 中为该菜单选项定义一个click事件处理程序。将清单 22-2 中的代码添加到home.js中,并在ready函数中绑定这个click事件处理程序。

清单 22-2。 点击事件处理程序获取相机菜单选项

cameraMenuItem_click: function (e) {
    nav.navigate("/pages/documents/cameraCapture.html");
},

在本书附带的源代码中,我还为用户添加了右击相机菜单选项并将其固定在开始屏幕上的功能(见图 22-6 )。因为我刚刚在第二十一章的中谈到了这一点,所以这里就不赘述了。您可以在本书的产品详细信息页面的源代码/下载选项卡上找到本章的代码示例(www.apress.com/9781430257790)。

9781430257790_Fig22-06.jpg

图 22-6 。将 Clok 相机固定在开始屏幕上

添加相机页面控件

虽然CameraCaptureUI类显示的用户界面是由 Windows 自己提供的,我们无法控制它提供的布局和功能,但仍然需要一个页面来允许用户预览捕获的图像并选择要添加到的文档库。在pages\documents文件夹中,创建一个名为cameraCapture.html的新页面控件。更新cameraCapture.html中的标题,将其改为Camera(见清单 22-3 )。

清单 22-3。 改变屏幕标题

<span class="pagetitle">Camera</span>

当你完成建立这个屏幕时,它会在屏幕的左边显示一个捕获的照片的预览,在右边,一个下拉列表选择照片将被添加到哪个项目的文档库。用清单 22-4 中的代码更新cameraCapture.html中的主section。如果用户的计算机没有摄像头,则会显示一条消息。

清单 22-4。 摄像头屏幕的布局

<section aria-label="Main content" role="main">
    <div id="cameraContainer">
        <div id="cameraPane">
            <img id="capturedImage" src="/img/camera-placeholder.png" />
        </div>
        <div id="controlsPane">
            <select id="projects">
                <option value="">Choose a project</option>
            </select>
            <button id="goToDocumentsButton" disabled="disabled"></button>
            <br />
            <button id="saveCameraCaptureButton">Save</button>
            <button id="discardCameraCaptureButton">Discard</button>
        </div>
    </div>
    <div id="noCamera" class="hidden">No camera is available on this computer.</div>
</section>

和往常一样,参考图片可以作为本书附带的源代码的一部分获得。用清单 22-5 中的 CSS 更新cameraCapture.css

清单 22-5。 造型相机屏幕

.cameraCapture section[role=main] {
    margin-left: 120px;
    margin-right: 120px;
}

.hidden {
    display: none;
}

.cameraCapture #cameraPane {
    float: left;
    width: 720px;
    height: 540px;
}

    .cameraCapture #cameraPane #capturedImage {
        max-width: 720px;
        max-height: 540px;
    }

.cameraCapture #controlsPane {
    float: left;
    margin-left: 10px;
}

    .cameraCapture #controlsPane #goToDocumentsButton {
        border: 0px;
        min-width: inherit;
        font-size: 1.5em;
    }

@media screen and (-ms-view-state: fullscreen-portrait) {
    .cameraCapture section[role=main] {
        margin-left: 20px;
        margin-right: 20px;
    }
}

在接下来的几节中,您将利用cameraCapture.js中的一些别名。与其一次添加一个,不如现在全部添加。将清单 22-6 中的代码添加到cameraCapture.js中。

清单 22-6。 添加一些别名

var appData = Windows.Storage.ApplicationData.current;
var createOption = Windows.Storage.CreationCollisionOption;
var capture = Windows.Media.Capture;
var devices = Windows.Devices.Enumeration;
var nav = WinJS.Navigation;
var storage = Clok.Data.Storage;

当应用处于快照状态时,Clok 的相机屏幕将不会启用。在第十七章中,你增加了一个名为DisableInSnappedView的函数,应该是从readyupdateLayout函数中调用的。将清单 22-7 中的代码从添加到到cameraCapture.js

清单 22-7。 禁用抓拍视图中的相机屏幕

ready: function (element, options) {
    Clok.Utilities.DisableInSnappedView();
},

updateLayout: function (element, viewState, lastViewState) {
    Clok.Utilities.DisableInSnappedView();
},

确定摄像机是否存在

尽管笔记本电脑和平板电脑内置摄像头越来越普遍,但很可能有些用户的电脑没有摄像头。在本节中,您将确定摄像机是否可用,并根据摄像机的存在初始化 Clok 的摄像机屏幕。

Windows.Devices.Enumeration 名称空间定义了许多类,您可以利用这些类为您的应用添加对各种类型设备的支持。这包括外部存储设备、音频输入和输出设备以及视频输入设备。视频设备是可以捕捉照片或视频的工具,如网络摄像头。关于这个名称空间及其包含的所有类的更多信息可以在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.enumeration.aspx)上找到。在这一节中,我们将利用Windows.Devices.Enumeration.DeviceInformation类来确定计算机是否有摄像头。将清单 22-8 中突出显示的代码添加到cameraCapture.js中的ready函数中。

清单 22-8。 确定摄像头是否存在并初始化屏幕

ready: function (element, options) {
    this.file = null;

    var deviceInfo = devices.DeviceInformation;
    return deviceInfo.findAllAsync(devices.DeviceClass.videoCapture)
        .then(function (found) {
            return found && found.length && found.length > 0;
        }, function error() {
            return false;
        }).then(function (exists) {
            this.showCameraControls(exists);
            if (exists) {
                this.bindProjects();
                capturedImage.addEventListener("click", this.capturedImage_click.bind(this));
                saveCameraCaptureButton.addEventListener("click",
                    this.saveCameraCaptureButton_click.bind(this));
                discardCameraCaptureButton.addEventListener("click",
                    this.discardCameraCaptureButton_click.bind(this));
                goToDocumentsButton.onclick = this.goToDocumentsButton_click.bind(this);
                projects.addEventListener("change", this.projects_change.bind(this));

                this.resetScreen();

                // TODO: automatically initiate capture

            }
        }.bind(this));

    Clok.Utilities.DisableInSnappedView();
},

这里,我使用了findAllAsync函数来枚举用户计算机上的所有视频捕获设备(网络摄像头)。如果有任何错误,或者没有发现错误,我稍后将展示的showCameraControls函数将向用户显示一条消息。然而,如果发现网络摄像头,showCameraControls功能将显示在清单 22-4 中定义的用户界面,一个活动项目的下拉列表将被填充,各种事件处理程序将被连接到它们各自的控件。这些步骤都是您通常会直接包含在ready函数中的典型步骤。因为这个屏幕只有在摄像机存在时才可用,所以我简单地将它们嵌套在一个条件中。在ready函数顶部的file变量将被用来保存对一个StorageFile对象的引用,该对象最终将从CameraCaptureUI接口返回。我稍后将回到这一点。

image 注意在 Clok 中,我们已经为用户提供了向文档库中添加文档(包括图像)的其他技术。在其他应用中,如果网络摄像头不可用,您可能需要考虑提供处理图像的替代方法,如文件打开选择器。

清单 22-8 中的代码引用了一些函数和事件处理程序。通过添加清单 22-9 中到cameraCapture.js的代码来定义这些。

清单 22-9。 摄像机屏幕的功能和事件处理程序

showCameraControls: function (show) {
    if (show) {
        WinJS.Utilities.removeClass(cameraContainer, "hidden");
        WinJS.Utilities.addClass(noCamera, "hidden");
    } else {
        WinJS.Utilities.addClass(cameraContainer, "hidden");
        WinJS.Utilities.removeClass(noCamera, "hidden");
    }
},

bindProjects: function () {
    projects.options.length = 1; // remove all except first project

    var activeProjects = storage.projects.filter(function (p) {
        return p.status === Clok.Data.ProjectStatuses.Active;
    });

    activeProjects.forEach(function (item) {
        var option = document.createElement("option");
        option.text = item.name + " (" + item.projectNumber + ")";
        option.title = item.clientName;
        option.value = item.id;
        projects.appendChild(option);
    });
},

projects_change: function (e) {
    if (!this.file) {
        saveCameraCaptureButton.disabled = true;
    } else {
        saveCameraCaptureButton.disabled = !projects.options[projects.selectedIndex].value;
    }

    var id = projects.options[projects.selectedIndex].value;
    goToDocumentsButton.disabled = !ClokUtilities.Guid.isGuid(id);
},

goToDocumentsButton_click: function (e) {
    var id = projects.options[projects.selectedIndex].value;
    if (ClokUtilities.Guid.isGuid(id)) {
        nav.navigate("/pages/documents/library.html", { projectId: id });
    }
},

discardCameraCaptureButton_click: function (e) {
    this.resetScreen();
},

resetScreen: function () {
    capturedImage.src = "/img/camera-placeholder.png";
    this.file = null;
    saveCameraCaptureButton.disabled = true;
    discardCameraCaptureButton.disabled = true;
},

清单 22-9 中的大部分代码类似于你已经添加到 Clok 中的代码。我特别强调了projects_change事件处理程序中的一个模块,这个模块可能不会立即显示出来。仅当从下拉列表中选择了一个项目并且图像已经被CameraCaptureUI接口捕获时,该块才启用保存按钮。

此外,我突出显示了resetScreen函数。顾名思义,该功能会将相机屏幕置于准备捕捉图像的状态。除了在第一次加载屏幕和用户丢弃照片时调用该函数之外,在下一节中,您将看到在用户捕获并保存照片后如何调用该函数。

使用 CameraCaptureUI 界面捕捉照片

要完成这一功能,还需要几个步骤。您仍然需要向用户显示CameraCaptureUI界面,检索捕获的图像并显示它,以便用户可以预览它,并实现将图像保存到项目文档库中的功能。通过添加清单 22-10 到cameraCapture.js中的代码,可以完成这三个剩余步骤中的前两个。

清单 22-10。 显示 CameraCaptureUI 界面

capturedImage_click: function (e) {
    this.showCameraCaptureUI();
},

showCameraCaptureUI: function () {
    var dialog = new capture.CameraCaptureUI();
    dialog.photoSettings.maxResolution =
        capture.CameraCaptureUIMaxPhotoResolution.highestAvailable;

    dialog.captureFileAsync(capture.CameraCaptureUIMode.photo)
        .done(function complete(file) {
            if (file) {
                var photoBlobUrl = URL.createObjectURL(file, { oneTimeOnly: true });
                capturedImage.src = photoBlobUrl;
                saveCameraCaptureButton.disabled =
                    !projects.options[projects.selectedIndex].value;
                discardCameraCaptureButton.disabled = false;
                this.file = file;
            } else {
                this.resetScreen();
            }
        }.bind(this), function error(err) {
            this.resetScreen();
        }.bind(this));
},

当用户点击预览图像时,将显示CameraCaptureUI界面。与FileOpenPicker类似,CameraCaptureUI界面是一个由 Windows 创建和管理的对话框。使用 promises,所选择的StorageFile,这次包含照相机刚刚捕获的照片,可作为done函数的complete参数的一个参数。在清单 22-10 中,我已经从该文件创建了一个对象 URL,并将预览图像的源设置为该 URL。如果已经从下拉列表中选择了一个项目,保存按钮被激活,并且文件的引用被存储在清单 22-8 中定义的file变量中。如果有任何错误,或者没有拍摄照片,则调用resetScreen函数将屏幕上的每个控件重置为其原始状态。

用户可以很容易地单击预览图像占位符来启动照片捕捉过程。然而,如果他们只是点击了 Clok 仪表板上的相机菜单选项,或者他们可能已经钉在开始屏幕上的次级磁贴,我们可以立即向他们显示CameraCaptureUI界面,而不需要他们再次点击。清单 22-8 包含一个 TODO 注释。用清单 22-11 中突出显示的代码替换cameraCapture.jsready函数中的注释。

清单 22-11。 自动显示 CameraCaptureUI 界面

if (exists) {
    // SNIPPED
    // only if navigated, not if back arrow
    if (!nav.canGoForward) {
        this.showCameraCaptureUI();
    }
}

image 注意当我最初编写这个功能时,我没有在条件中包含对showCameraCapture的调用。然而,在点击清单 22-4 中添加的goToDocumentsButton,然后点击每个页面控件上包含的后退箭头之后,在那种情况下加载CameraCaptureUI界面对我来说似乎很尴尬。通过检查WinJS.Navigation.canGoForward属性的值,您可以确定是否加载了摄像头屏幕,这是单击返回箭头的结果。

这个难题的最后一部分是实现将照片保存到所选项目的文档库中的功能。将清单 22-12 中的代码添加到cameraCapture.js中。

清单 22-12。 保存照片到文档库

getProjectFolder: function (projectId) {
    return appData.localFolder
        .createFolderAsync("projectDocs", createOption.openIfExists)
        .then(function (folder) {
            return folder.createFolderAsync(projectId.toString(), createOption.openIfExists)
        });
},

saveCameraCaptureButton_click: function (e) {
    var dateFormatString = "{year.full}{month.integer(2)}{day.integer(2)}"
        + "-{hour.integer(2)}{minute.integer(2)}{second.integer(2)}";
    var clockIdentifiers = Windows.Globalization.ClockIdentifiers;

    var formatting = Windows.Globalization.DateTimeFormatting;
    var formatterTemplate = new formatting.DateTimeFormatter(dateFormatString);
    var formatter = new formatting.DateTimeFormatter(formatterTemplate.patterns[0],
                formatterTemplate.languages,
                formatterTemplate.geographicRegion,
                formatterTemplate.calendar,
                clockIdentifiers.twentyFourHour);

    var filename = formatter.format(new Date()) + ".png";

    var projectId = projects.options[projects.selectedIndex].value;

    this.getProjectFolder(projectId)
        .then(function (projectFolder) {
            return this.file.copyAsync(projectFolder,
                filename,
                createOption.generateUniqueName);
        }.bind(this)).then(function (file) {
            this.resetScreen();
        }.bind(this));
},

从您在第十六章的中创建文档库的工作中,以及从您在第十九章的中添加的使 Clok 成为共享目标的代码中,您应该熟悉getProjectFolder函数。在根据当前日期和时间为filename变量生成一个值后,由file变量引用的照片,使用第十六章中介绍的copyAsync函数被复制到项目的文档库中。然后调用resetScreen功能,允许用户快速拍摄另一张照片。

现在运行 Clok 并点击 Clok 仪表盘屏幕上的相机菜单选项。如果你有一台相机并且已经授权 Clok 使用它,你会看到CameraCaptureUI界面(见图 22-7 )。

9781430257790_Fig22-07.jpg

图 22-7 。即将捕获正在进行的一些工作的照片

拍照后,您将看到该照片的预览,并能够选择将其保存到哪个项目的文档库(参见图 22-8 )。

9781430257790_Fig22-08.jpg

图 22-8 。预览捕获的照片

最后,为了验证一切都按预期工作,导航到该文档库。您可以选择使用相机屏幕上的回形针图标作为快捷方式。您应该可以在文档库中看到您的照片(参见图 22-9 )。

9781430257790_Fig22-09.jpg

图 22-9 。捕获的照片,保存到文档库中

在本例的大部分时间里,我假设用户是另一个软件开发人员,他/她正在用自己的平板电脑运行 Clok,参加一个关于他/她正在进行的项目的会议。实际上,这个功能可以很容易地被平面设计师用来拍摄可能启发他或她的项目设计的照片,或者甚至被景观承包商用来拍摄景观项目前后的照片。用户甚至可以是捕捉他或她当前正在进行的工作的照片的作者。

媒体捕获类概述

CameraCaptureUI类提供了一个简单的方法来将一个附加的摄像机集成到你的应用中。然而,这个界面是由 Windows 创建和管理的,所以您对它没有任何控制权。如果你对图 22-7 (前面)中显示的全屏捕捉体验不满意,那么你就不走运了。

不完全是。如果您需要对照片捕捉体验有更多的控制,那么您应该使用MediaCapture类,而不是使用CameraCaptureUI类。通过这个类,您可以将实时视频预览和图像捕捉功能嵌入到您自己的用户界面中。

我不会在本书中实现这个类,但是我会在这里简单讨论一下。在 Windows SDK 示例包(http://msdn.microsoft.com/en-US/windows/apps/br229516)包含的示例中,有一个叫做使用捕获设备进行媒体捕获的示例。这个项目展示了几个将相机集成到应用用户界面中的例子。看一看BasicCapture.html。在该文件中,您将看到一个如清单 22-13 中的所示的video元素。

清单 22-13。 来自 MediaCapture 示例项目的视频元素

<video width="320" height="240" id="previewVideo1" style="border: 1px solid black"> </video>

这是一个标准的 HTML5 video元素。在关联的 JavaScript 文件BasicCapture.js中,创建了一个名为mediaCaptureMgrMediaCapture对象,并在该对象上设置了许多属性。一旦它被配置,来自清单 22-13 的video元素的src属性被设置,引用mediaCaptureMgr对象(见清单 22-14 )。

清单 22-14。 设置视频源

video.src = URL.createObjectURL(mediaCaptureMgr, { oneTimeOnly: true });

为了从这个mediaCaptureMgr对象中捕获图像,调用了capturePhotoToStorageFileAsync函数。类似于清单 22-10 中的captureFileAsync函数,代表捕获图像的StorageFile对象作为done函数的complete参数的一个参数。

我在本节中排除了许多细节,但是引用的示例相当简单。如果将相机捕捉功能集成到您自己的应用的 UI 中的能力很重要,那么我鼓励您仔细看看示例项目。此外,关于MediaCapture类的更多信息可以在 MSDN ( http://msdn.microsoft.com/library/windows/apps/Windows.Media.Capture.MediaCapture.aspx)上找到。

位置

在 Windows 8 中,用户电脑的位置由 Windows 位置提供商提供。位置提供者可以使用多种不同的方法来确定计算机的位置,该位置由纬度和经度测量值以及这些测量值的准确性指示符来指示。Windows 8 中的提供商使用以下技术尝试确定位置,从最不准确到最准确:

  • IP 地址数据
  • WiFi 三角剖分〔??〕〔??〕
  • 全球定位系统

提供商将报告可用的最精确的测量值。例如,如果 GPS 设备存在于计算机中,那么将返回这些结果,而不是 IP 地址查找的结果。MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/hh464919.aspx)上有更多关于 Windows 位置提供商的信息。

在这一节中,我将展示如何为用户提供一种从他们当前位置获取驾驶方向的简单方法。

应用清单更改

与相机一样,访问用户的位置是一个潜在的安全问题。您必须在项目的应用清单中指示您打算访问用户的位置。与相机一样,对清单的更改也是一个简单的复选框。打开package.appxmanifest并切换到功能选项卡。在功能列表中,检查位置项(参见图 22-10 )。

9781430257790_Fig22-10.jpg

图 22-10 。指定定位能力

更新方向屏幕

实现这一功能只需很少的改动。第一步是在方向屏幕上添加一个按钮,用于确定用户的当前位置。将清单 22-15 中突出显示的代码添加到directions.html中。

清单 22-15。 添加按钮获取当前位置

<div class="formField">
    <label for="fromLocation">From</label><br />
    <input id="fromLocation">
    <button id="getLocationButton"> & #xe1d2;</button>
</div>

然后将清单 22-16 中的新 CSS 规则添加到directions.css中。

清单 22-16。 造型按钮

#locationsPane #getLocationButton {
    border: 0px;
    min-width: inherit;
}

虽然这个按钮还没有做任何事情,但是你可以运行 Clok 并导航到方向屏幕来看看它看起来怎么样(见图 22-11 )。

9781430257790_Fig22-11.jpg

图 22-11 。获取当前位置的按钮

在下一节中,我将向您展示当用户单击这个按钮时,如何获取他或她的当前位置。

确定当前位置

如果不能确定用户的位置,让用户点击按钮是没有意义的。在本节中,您将添加一些代码,以便仅当用户的位置可用时才启用按钮,还将添加一些代码,以便在单击按钮时用他或她的位置填充 From 字段。将清单 22-17 中高亮显示的代码添加到directions.jsready函数中。

清单 22-17。 变为就绪功能

printCommand.onclick = this.printCommand_click.bind(this);

this.currentCoords = null;
getLocationButton.disabled = true;
this.checkForGeoposition();
getLocationButton.onclick = this.getLocationButton_click.bind(this);

fromLocation.value = app.sessionState.directionsFromLocation || "";

checkForGeoposition函数是这个特性的主力。将清单 22-18 中的功能添加到directions.js中。

清单 22-18。 请求用户的当前位置

checkForGeoposition: function () {
    var locator = new Windows.Devices.Geolocation.Geolocator();
    var positionStatus = Windows.Devices.Geolocation.PositionStatus;

    if (locator != null) {
        locator.getGeopositionAsync()
            .then(function (position) {
                this.currentCoords = position.coordinate;
                getLocationButton.disabled = (locator.locationStatus !== positionStatus.ready);
            }.bind(this));
    }
},

这个函数获取名为locatorWindows.Devices.Geolocation.Geolocator类的一个实例。如果locator不存在,上一节添加的新按钮永远不会启用。然而,如果getGeopositionAsync函数返回一个有效位置,按钮被激活,坐标被存储在currentCoords变量中。

幸运的是,Bing Maps API 完全能够使用坐标,而不是地址,作为起点或目的地。因此,所需的最后一步是在单击按钮时用用户的纬度和经度填充 From 字段。将清单 22-19 中的代码添加到directions.js中。

清单 22-19。 填充从字段

getLocationButton_click: function (e) {
    fromLocation.value = this.currentCoords.latitude.toString()
        + ", " + this.currentCoords.longitude.toString();
},

现在,当您运行 Clok 并单击按钮时,您的纬度和经度将显示在 From 字段中。获取方向按钮将把这些坐标传递给 Bing Maps API,并检索从您当前位置到客户办公室的行驶方向(参见图 22-12 )。

9781430257790_Fig22-12.jpg

图 22-12 。获取从当前位置到客户办公室的路线

读者的家庭作业——作业#1

如您所见,只需很少的代码,您就可以将来自 Windows 位置提供程序的数据集成到您的应用中。不过,需要记住的一点是位置数据的准确性。正如我上面提到的,位置提供者可以从许多来源检索位置数据。您将总是收到可用的最准确的数据,但是如果 IP 地址数据是所有可用的数据,则可能只精确到几英里以内。根据您的情况,您可能希望您的用户可以接受这种可能的不准确性,但是在驾驶方向的情况下,让起始位置偏离多达 10 英里并不是一个好的体验。

当使用getGeopositionAsync功能检索坐标时,包含了这些坐标的精度指标,以米为单位。在本书附带的源代码中,我添加了一个条件,只有当this.currentCoords.accuracy的值在 200 米以内时,才启用getLocationButton

有关使用Geolocator类和其他位置相关类的更多信息,请访问 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.geolocation.aspx)。

读者的家庭作业——作业#2

在本章中,我向您展示了如何使用Geolocator类的checkForGeoposition函数来请求用户的当前位置。Geolocator类还有一个名为positionchanged的事件,当检测到用户位置发生变化时会引发该事件。虽然我还没有在 Clok 中构建任何利用该事件的特性,但是存在一些可能性,您可以选择添加到 Clok 中。例如:

  • 您可以更新方向屏幕,以便根据用户驾车前往目的地时的当前位置,突出显示路线中的当前步骤。
  • 您可以添加一个 toast 通知,当用户到达他或她的一个客户端的位置时显示该通知。单击此通知可能会启动预先选择了该项目的 Clok 仪表板屏幕。

关于positionchanged事件的更多信息可在 MSDN ( http://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.geolocation.geolocator.positionchanged.aspx)上获得。

结论

借助 Windows 8,微软可以轻松地将您的应用与可能连接到用户计算机的各种硬件相集成。相机和位置数据在许多现代计算机中都很容易获得,只需少量代码,您就可以在应用中使用它们。在本章中,我只触及了这个功能的表面。在许多情况下,这些简单的实现足以改善用户对应用的体验。但是,请记住,如果需要,还可以使用更高级的功能。