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

在 Windows* 8 桌面应用中使用 WPF 实现多用户多点触控场景

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2014年1月17日

CPOL

14分钟阅读

viewsIcon

19266

本文将介绍一个示例应用程序(在本例中是一个关于元素周期表的问答游戏),该应用程序支持多用户、多点触控功能,并针对大型触摸屏显示器进行了优化。

总结 

本文将介绍一个示例应用程序(在本例中是一个关于元素周期表的问答游戏),该应用程序支持多用户、多点触控功能,并针对大型触摸屏显示器进行了优化。通过使用用户控件和触摸事件,我们可以实现多个用户同时玩游戏的情景。

Windows Presentation Foundation (WPF) 提供了一个强大的触摸框架,使我们能够处理低级触摸事件,并支持从简单的触摸滚动到多用户场景的各种场景。此游戏中有两个用户可以触摸、滚动和单击的区域,其余 UI 保持响应。最后,此应用程序使用 XAML 和 C# 设计和构建,并遵循 Model-View-ViewModel 软件开发模式的原则。

在 Windows Presentation Foundation 中支持大型触摸显示器和多用户

WPF 是为 Windows 桌面系统构建业务线应用程序的出色框架,但它也可用于开发现代、动态的应用程序。您可以应用为 WPF 应用程序设计所使用的许多相同原则,只需稍作调整,即可使其在大尺寸显示器上友好易用。

XAML 标记语言的一个基本原则是其控件的无外观性。这意味着控件的外观和样式与其实现是分离的。控件作者可以为控件提供默认样式,但此样式可以轻松覆盖。如果您在 XAML 中放置了一个样式(隐式或显式),它将附加到随框架一起提供的基样式。您还可以使用 Visual Studio* 2012 中的模板提取功能来复制 .NET 框架随附的样式和模板,并将其用作起点。

我们来看一个例子

为了创建一个带有自定义关闭按钮的窗口,我在 Visual Studio 中创建了一个空的 WPF 项目,并修改了 MainWindow.xaml 文件,如下所示:

<Window x:Class="ExampleApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" WindowStyle="None">
    <Grid>
        <Button HorizontalAlignment="Right" VerticalAlignment="Top" Content="Close" Click="Close_Window" />
    </Grid>
</Window>

然后,我编写了一个 C# 方法来处理窗口关闭事件:

        private void Close_Window(object sender, RoutedEventArgs e)
        {
            this.Close();
        }

这将创建一个如下所示的窗口:

由于我们使用的是 Windows 8 平台,因此可以使用 Segoe UI Symbol 字体在按钮中放置关闭符号。您可以在 Windows 字符映射表中的 Segoe UI Symbol 字体下查找所需的符号。

现在我有了字符代码,就可以开始自定义按钮了。首先,我在按钮中添加了关闭符号:

        <Button HorizontalAlignment="Right" VerticalAlignment="Top" FontFamily="Segoe UI Symbol" Content="" Click="Close_Window" />

我还想通过应用 XAML 样式来为按钮设置样式,使其更适合触摸。这可以通过创建继承样式来完成,该样式位于其视觉层次结构中按钮的任何位置。我将 Button 样式添加到 Window 的资源中,以便窗口中的任何按钮都可以使用它:

        <Style TargetType="Button">
            <Setter Property="BorderBrush" Value="White" />
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="Foreground" Value="White" />
            <Setter Property="BorderThickness" Value="2" />
            <Setter Property="Padding" Value="12,8" />
            <Setter Property="FontSize" Value="24" />
            <Setter Property="FontWeight" Value="Bold" />
        </Style>

为了说明这一点,我将窗口的背景颜色更改为白色。上面的样式将生成如下所示的按钮:

您可以随时更改样式,例如,使其具有更大的图标和更少的内边距。对于按钮和文本内容,您可能会发现使用固定的内边距、边距和尺寸值,因为它们很少更改。如果您希望文本内容真正具有响应性,您可以始终将文本内容放在 ViewBox 中,使其相对于窗口缩放。这对于大多数大屏幕应用程序来说不是必需的,但如果您的应用程序将在极端的屏幕分辨率下运行,则需要考虑这一点。

对于大多数 UI 元素,您需要根据相对大小来设置内边距和边距。这可以通过使用 Grid 作为布局系统来实现。例如,在演示应用程序中,我们希望在每个元素周期表元素周围留出非常小的空间。我可以在每个项目周围使用 1px 的内边距,但内边距的宽度在不同用户的大显示器和小显示器上的外观会有所不同。您还必须考虑您的最终用户可能使用的监视器和分辨率比您的开发环境支持的要大。为了解决这个问题,我使用网格创建行和列来表示内边距。例如,我可以创建一个具有 3 行和 3 列的网格,如下所示:

<Grid x:Name="tableRoot">
            <Grid.RowDefinitions>
                <RowDefinition Height="0.01*"/>
                <RowDefinition Height="0.98*"/>
                <RowDefinition Height="0.01*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="0.01*"/>
                <ColumnDefinition Width="0.98*"/>
                <ColumnDefinition Width="0.01*"/>
            </Grid.ColumnDefinitions></Grid>

在网格定义大小方面,有三种选项可用。您可以使用绝对高度或宽度进行静态大小调整,根据内容来测量和确定大小的自动大小调整,或者相对大小调整,或者您可以混合使用不同的选项。在我们的示例中,我们大量使用相对大小。XAML 引擎会汇总相对大小的值,并分配相当于该单个值与总值之比的大小。例如,如果列的大小如下:

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="4*"/>
                <ColumnDefinition Width="7*"/>
                <ColumnDefinition Width="9*"/>
            </Grid.ColumnDefinitions>

列宽 (4, 7, 和 9) 的总和是 20。所以每个宽度是每个值与总和 20 的比率。第一列将是 4/20 (20%),第二列将是 7/20 (35%),最后一列将是 9/20 (45%)。虽然这可以正常工作,但为了简单起见,最好使所有列(或行)的总和为 100 或 1。在第一个示例中,我们确保高度和宽度加起来等于 1。列和行的索引是基于零的,所以我们可以将内容放在第 1 列和第 1 行,它将具有 1% 的内边距。无论分辨率如何,这都是 1%,并且无论分辨率如何,对用户来说外观都相对相同。设置为静态大小的内边距在具有高分辨率的大型触摸屏显示器上会比您在开发过程中预期的要薄得多。在元素周期表应用程序中,您可以在浏览表格本身时看到这种 1% 的内边距:

您还可以为应用程序启用触摸滚动,使其更具响应性。开箱即用,WPF 允许您用手指滚动列表元素。ScrollViewer 会将您的滚动限制在每个元素内,所以它更像是元素之间的滑动。如果您想启用“平滑”滚动,应该设置 ScrollViewer 的 PanningMode。默认情况下,PanningMode 设置为 None。通过将其设置为 VerticalOnly HorizontalOnly,您将启用列表中项目的平滑滚动。在元素周期表应用程序中,ScrollViewer.PanningMode 附加属性用于在典型的 ListView 上启用此场景。我还将 ScrollViewer.CanContentScroll 属性设置为 false,这样项目就不会吸附,用户就可以用手指在项目之间滑动。

<ListView x:Name="SecondBox" Background="Transparent" ItemsSource="{Binding Source={StaticResource PeriodicData}}" 
                  ScrollViewer.VerticalScrollBarVisibility="Disabled" 
                  ScrollViewer.HorizontalScrollBarVisibility="Visible"
                  ScrollViewer.PanningMode="HorizontalOnly" 
                  ScrollViewer.CanContentScroll="False"></ListView>

应用程序中使用的 ListView 用于查看如下的元素周期表项目:

最后,WPF 允许我们使用自 Windows 7 以来一直存在的内置触摸支持。当您没有专门处理触摸事件(如 TappedManipulationDeltaManipulationEnded)时,Windows 会将触摸输入识别为鼠标。这使您可以使用 Click 事件处理程序来处理用户点击上述任何项目的情况。这还最大限度地减少了支持触摸和鼠标所需的代码量。

由于触摸支持是在非常低的级别实现的,因此 WPF 平台不会按用户或集群对触摸进行分组。要绕过这一点,您通常会看到控件作者使用视觉提示(如边框或框)来指示用户应在特定区域内触摸。为了支持多用户,我们可以将支持触摸的控件放在 UserControl 中。用于查找游戏元素周期表元素的浏览器是一个 UserControl,因此我们可以通过将逻辑放入 UserControl 来在屏幕上放置任意数量的它。

Model-View-ViewModel 模式

构建应用程序时,很容易将代码写在 xaml.cs 文件中然后就此结束,但我们希望最大限度地重用代码并构建一个真正模块化的应用程序。我们可以通过利用 MVVM 设计模式来实现这一点。在元素周期表应用程序中,每个屏幕都绑定到一个 ViewModel。它包含数据绑定信息并控制不同视图的行为。我们还有一个使用 XAML 的数据源,需要操作该数据源来运行游戏。有关数据源的详细信息将在本文稍后讨论。

由于 MVVM 是一种流行的设计模式,它可以在 WPF、Windows 应用商店和 Windows Phone 平台上使用。为了支持此场景,我们可以将我们的 Models 和 ViewModels 放入可移植类库 (PCL),这些库可以被所有这些平台引用。PCL 包含所有这些平台之间的通用功能和命名空间,并允许您编写跨平台代码。许多工具和库(如 Ninject、PRISM 的 EventAggregator 等)可通过 NuGet 获得,并可以在 PCL 中引用,以便您创建大型应用程序。如果您需要支持新平台,只需创建新的 Views 并引用现有的 ViewModels 和 Models。

此应用程序正在解析一个静态数据文件,其中包含有关如何渲染元素周期表的信息。Models 了解 WPF 中的类,因此 PCL 在此示例中不适用。

在此应用程序中,我们使用 PRISM 框架来利用已构建的 MVVM 开发模块。

对于主页,我们有一个 BaseViewModel,它有一个命令。ExitCommand 在执行时会关闭应用程序。通过将数据绑定应用于 Button 的 Command 依赖属性,我们可以将此命令绑定到本文前面提到的按钮。

    public class BaseViewModel : NotificationObject
    {
        public BaseViewModel()
        {
            this.ExitCommand = new DelegateCommand(ExitExecute);
        }

        public DelegateCommand ExitCommand { get; private set; }

        private void ExitExecute()
        {
            Application.Current.Shutdown();
        }
    }

首先,ViewModel 继承自 PRISM 的 NotificationObject 类。此类包含所有逻辑,用于在 ViewModel 的属性更新时通知 View。这是通过实现 INotifyPropertyChanged 接口来实现的。如果您想查看 INotifyPropertyChanged 的优秀最佳实践实现,可以查看 PRISM 项目的源代码,了解 Microsoft 团队如何实现此接口。

接下来,我们使用 PRISM 框架中的 DelegateCommand 类。DelegateCommandICommand 接口的实现,它是 WPF 中命令的核心。此类可用于处理按钮的单击事件和确定按钮是否启用的逻辑。此支持不仅适用于按钮,而且是 ICommand 主要的用例。

在我们的 BaseViewModel 类中,我们创建了 DelegateCommand 类的一个新实例,并将 ExitExecute 操作传递给它,以便在调用 Command 时(通过按按钮)执行。

由于您可以从任何屏幕关闭应用程序,因此所有其他页面都继承自 BaseViewModel 类。为了将所有游戏相关逻辑集中在一起,1 人游戏和 2 人游戏都使用继承自 GameViewModel 类的 ViewModels,而 GameViewModel 类又继承自 BaseViewModel

GameViewModel 类实现了公有的属性,这些属性用于游戏中。以下是游戏屏幕上显示的几个示例字段:

例如,我们有一个 RoundTimeLeft 属性,显示一轮剩余的时间。该属性的类型为 TimeSpan,它使用私有的后备字段。设置属性时,我们使用 NotificationObject 类的方法来通知 View 层 ViewModel 属性已更新。

        private TimeSpan _roundTimeLeft;
        public TimeSpan RoundTimeLeft
        {
            get { return _roundTimeLeft; }
            private set
            {
                _roundTimeLeft = value;
                RaisePropertyChanged(() => RoundTimeLeft);
            }
        }

这在您希望 View 在更新单个字段/属性时刷新多个属性的情况下特别有用。此外,作为高级应用程序的性能改进,通常会在通知 View 您的属性已更改之前检查值是否已更改。下面是一个在 ViewModel 中使用的 HintItem 属性和 Hint 属性的示例。Hint 属性是中心显示的符号,我们希望在将新的 HintItem 存储到 ViewModel 中时更新该文本。这是通过让 View 知道 Hint 属性已更新来实现的:

        private PeriodicItem _hintItem;
        public string Hint
        {
            get
            {
                return this.HintItem != null ? this.HintItem.Abbreviation : string.Empty;
            }
        }

        public PeriodicItem HintItem
        {
            get { return _hintItem; }
            private set
            {
                _hintItem = value;
                RaisePropertyChanged(() => Hint);
                RaisePropertyChanged(() => HintItem);
            }
        }

Model-View-ViewModel 模式非常强大,它在处理应用程序时提供了可测试性和扩展的代码重用。无论您是处理业务线应用程序还是触摸应用程序,都可以应用此模式。GameViewModel 类使用计时器和循环来处理游戏的执行。OnePlayerViewModelTwoPlayersViewModel 都继承自 GameViewModel 并添加了每个游戏类型的特定逻辑。该应用程序还有一个 DesignGameViewModel,它具有一组静态属性,因此我们可以在不运行应用程序的情况下查看游戏的设计时外观。

构建沉浸式 WPF 应用程序的技巧与窍门

此应用程序中有几个 XAML 技巧,用于使其在视觉上引人注目且适合触摸。有些很常见,但有几个值得强调,因为它们使用了一些 WPF 和 XAML 的最佳功能。

首先,PeriodicTable 本身就是一个 WPF UserControl。这最大限度地实现了代码重用,因为该控件可以简单地放置在任何 WPF Window 上。在控件内部,使用依赖属性,这样您就可以设置控件的功能,并将这些功能公开给外部用于数据绑定。例如,PeriodicTable 有两种状态。ZoomedOut 是您看到整个表格时:

ZoomedIn 是您看到详细列表时。从 ZoomedOut 视图单击一个周期组后,游戏会跳转到 ZoomedIn 列表中的该组。右下角还有一个按钮用于缩回:

为了实现这一点,有两个列表视图代表每个“视图”。创建一个依赖属性,该属性将公开任何人都可以设置的属性。然后创建一个 PropertyChanged 事件处理程序,以便控件可以响应代码和数据绑定在同一位置进行的所有更改:

        public static readonly DependencyProperty IsZoomedInProperty = DependencyProperty.Register(
            "IsZoomedIn", typeof(bool), typeof(PeriodicTable),
            new PropertyMetadata(false, ZoomedInChanged)
        );

        public bool IsZoomedIn
        {
            get { return (bool)GetValue(IsZoomedInProperty); }
            set { SetValue(IsZoomedInProperty, value); }
        }

        public void SetZoom(bool isZoomedIn)
        {
            if (IsZoomedIn)
            {
                FirstContainer.Visibility = Visibility.Collapsed;
                SecondContainer.Visibility = Visibility.Visible;
            }
            else
            {
                FirstContainer.Visibility = Visibility.Visible;
                SecondContainer.Visibility = Visibility.Collapsed;
            }
        }

此依赖属性在 TwoPlayerView 中使用,以便我们可以将 Player Two 的放大状态绑定到 ViewModel 中名为 PlayerTwoZoomedIn 的布尔值:

<local:PeriodicTable x:Name="playerTwoTable" IsZoomedIn="{Binding PlayerTwoZoomedIn, Mode=TwoWay}"></local:PeriodicTable>

此实现失去了将控件的自定义功能与 ViewModel 中的任何内容绑定的灵活性。在我们的应用程序中,我们需要将 PlayerTwoZoomedIn(和 PlayerOneZoomedIn)设置为 false,当回合或游戏重置时。

XAML 也被大量用于存储此应用程序中的数据。虽然可以创建数据库或文本文件,但将元素周期表的数据存储为 XAML 似乎更易读。由于 XAML 只是 CLR 对象的 XML 表示形式,因此我们可以创建模型类和相应的 XAML 元素。然后,我们可以将其存储在 XAML 资源字典中,并在运行时(如果您愿意,也可以在设计时)加载为数据。

例如,我们有一个 PeriodicItems 类,它有一个非常简单的定义,并由更简单的 XAML 表示:

    public class PeriodicItem
    {
        public string Title { get; set; }

        public string Abbreviation { get; set; }

        public int Number { get; set; }
    }
<local:PeriodicItem Abbreviation="Sc" Title="Scandium" Number="21" />
<local:PeriodicItem Abbreviation="Ti" Title="Titanium" Number="22" />

这使得定义元素周期表变得容易且易于阅读。您可以在 Data 文件夹中找到应用程序中使用的所有元素周期表元素,文件名为 PeriodicTableDataSource.xaml。以下是该文件中定义的周期组示例。

            <local:PeriodicGroup Key="Outer Transition Elements">
                <local:PeriodicGroup.Items>
                    <local:PeriodicItem Abbreviation="Ni" Title="Nickel" Number="28" />
                    <local:PeriodicItem Abbreviation="Cu" Title="Copper" Number="29" />
                    <local:PeriodicItem Abbreviation="Zn" Title="Zinc" Number="30" />
                    <local:PeriodicItem Abbreviation="Y" Title="Yttrium" Number="39" />
                </local:PeriodicGroup.Items>
            </local:PeriodicGroup>

因此,元素周期表数据是动态的,可以通过简单地更新 .xaml 文件来修改。您还可以同时在设计和运行时视图中使用相同的数据,因为它已被编译并作为 XAML 的资源可用。

摘要

在 Windows 8 桌面环境中,绝对有可能构建一个支持大量数据和高级触摸场景的应用程序。XAML 是一种强大的标记语言,它不仅可以定义动态视图,还可以以一种易于阅读、理解和解析的通用格式来建模数据。您可以使用成熟的 WPF 平台立即构建触摸应用程序。

Intel 和 Intel 标志是 Intel Corporation 在美国和/或其他国家/地区的商标。

版权所有 © 2013 英特尔公司。保留所有权利。

*其他名称和品牌可能被声明为他人的财产。

其他信息和相关内容

© . All rights reserved.