Clearer - 一款由手势驱动的Windows 8待办事项应用






4.97/5 (14投票s)
本文介绍了我将一个新颖的手势驱动的 Windows Phone 应用移植到 Windows 8 的经验。
目录
引言
很久以前,我写了一篇 CodeProject 文章,介绍了一个手势驱动的 Windows Phone 待办事项列表应用程序,它摒弃了按钮和复选框的使用,转而采用滑动、捏合等手势。Windows 8 开发与 Windows Phone 非常相似,两者都使用 XAML、C#,并且具有相似的应用程序生命周期。在本文中,我将描述我将 Windows Phone 应用程序移植到 Windows 8 以便将其添加到 Windows 应用商店的经验。
本文是我参加 CodeProject 超级本比赛的参赛作品,但我希望它本身也能作为一篇独立的文章。因此,我详细而诚实地叙述了我在 Windows 8 上的经历,包括完整的源代码。
从 Windows Phone 过渡到 Windows 8 并非一帆风顺,有一些功能我无法移植过来。然而,也有许多新机会可以探索,包括动态磁贴、搜索协议和分屏视图。
Windows Phone 应用程序深受i
Realmac 软件的 Phone 应用 Clear 的启发,我在此对他们提出的驱动其界面的新颖手势表示充分的肯定。如果您有 iPhone,请购买他们的应用程序,我买了……它提供了极好的思考素材。
我编写的 Windows Phone 应用程序有一个非常简单的视图模型,它只是一个简单的模型对象 `ToDoItem` 的 ObservableCollection。应用程序中的大部分代码都用于实现各种手势。
移植代码
我移植应用程序的第一步是启动 Visual Studio 2012,创建一个新的空白项目,然后将视图模型和 UI 交互逻辑从 Windows Phone 应用程序复制过来。该应用程序的结构方式是,手势使用类似于附加行为的模式添加,因此可以通过一行代码轻松添加或删除它们。我将各种组件缓慢地添加到新创建的 Windows 8 应用程序中,并在遇到问题时解决它们。
正如预期的那样,视图模型只需要很少的修改即可用于 Windows 8 构建。我唯一需要做的更改是将一个 `Color` 类型的属性的命名空间更改为
System.Windows.Media.Color => Windows.UI.Color System.Windows.Media.Color => Windows.UI.Color
在视图模型就位后,我向 `MainPage.xaml` 添加了一些待办事项,并将其设置为 `DataContext`:
private ToDoListViewModel _viewModel;
public MainPage()
{
this.InitializeComponent();
_viewModel = new ToDoListViewModel();
_viewModel.Items.Add(new ToDoItem("Feed the cat"));
_viewModel.Items.Add(new ToDoItem("Buy eggs"));
...
_viewModel.Items.Add(new ToDoItem("Simplify my life"));
this.DataContext = _viewModel;
}
用于 Windows Phone 版待办事项列表应用程序的 XAML 非常简单,包含一个用于渲染上述项目列表的 `ItemsControl`,以及每个项目的相对简单的模板。对于 Windows Phone,我在每个项目上添加了一个微妙的渐变,但对于 Windows 8,我决定采用更简单的“平面”外观。XAML 如下所示:
<Page ...>
<Page.Resources>
<conv:ColorToBrushConverter x:Key="ColorToBrushConverter"/>
<conv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<DataTemplate x:Key="toDoItemTemplate">
<Border Height="55" x:Name="todoItem">
<Grid Margin="2"
Background="{Binding Path=Color, Converter={StaticResource ColorToBrushConverter}}">
<TextBlock Text="{Binding Text, Mode=TwoWay}"
FontSize="25" TextTrimming="WordEllipsis"
Margin="10 0 0 0"
Foreground="White"
VerticalAlignment="Center"
Grid.Column="1"/>
<Line Visibility="{Binding Path=Completed, Converter={StaticResource BoolToVisibilityConverter}}"
X1="0" Y1="0" X2="1" Y2="0"
Stretch="UniformToFill"
Stroke="White" StrokeThickness="2"
Margin="8,5,8,0"
Grid.Column="1"/>
</Grid>
</Border>
</DataTemplate>
</Page.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Path=Items}"
ItemTemplate="{StaticResource toDoItemTemplate}"
x:Name="todoList">
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer x:Name="scrollViewer">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
</Grid>
</Page>
- `BoolToVisibility` 和 `ColorToBrush` 转换器需要进行一些小的更改,Windows 8 中值转换器的参数略有不同(第四个参数是 `string` 而不是 `CultureInfo` 实例)。
- Windows Phone 版本中的 `ScrollViewer` 设置了 `ManipulationMode` 为 `ManipulationMode.Control`。这确保了滚动查看器位置的更新在 UI 线程而不是滚动查看器线程上处理(这在 Mango 版本中添加,以提高滚动性能)。Windows Phone 应用程序需要 `ManipulationMode.Control` 来支持下拉添加新交互。Windows 8 处理滚动的方式截然不同,我们稍后会看到!
Windows 8 UI 如下所示:
为更大的屏幕和分屏视图量身定制
正如你在上面的屏幕截图中看到的,简单地将小手机屏幕的界面复制并缩放到较大的平板电脑外形尺寸并不一定有效。为了利用更大的屏幕,我将界面分成了两半:右侧渲染列表,左侧渲染每个项目的详细信息。
我还添加了一个背景图像,为界面增加了一些“纹理”。
为了支持屏幕左侧的“详细信息”视图,我在视图模型中添加了一个 `SelectedItem`。
/// <summary>
/// A collection of todo items
/// </summary>
public class ToDoListViewModel : INotifyPropertyChanged
{
private ToDoItem _selectedItem;
…
public ToDoItem SelectedItem
{
get
{
return _selectedItem;
}
set
{
_selectedItem = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
public ObservableCollectionEx<ToDoItem> Items
{
…
}
}
然后,我用 `ListBox` 替换了渲染项目列表的 `ItemsControl`,并相应地绑定了 `ItemsSource` 和 `SelectedItem` 属性。
<ListBox ItemsSource="{Binding Items}"
SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}"
x:Name="todoList">
...
</ListBox>
然后,屏幕左侧“详细信息”区域的根元素 `Grid` 的 `DataContext` 绑定到视图模型的 `SelectedItem` 属性。然后,“详细信息”区域内的子元素可以直接绑定到选定的待办事项的属性:
<Grid x:Name="itemDetailGrid"
DataContext="{Binding SelectedItem}">
<Image Source="Assets/StoreLogo.png"
Width="50" Height="50"
HorizontalAlignment="Left"/>
<TextBlock Text="Clearer" FontSize="55"
Margin="60 20 0 20"
VerticalAlignment="Center"/>
<TextBox Text="{Binding Text, Mode=TwoWay}"
Grid.Row="1" FontSize="25"/>
<TextBox Text="{Binding Description, Mode=TwoWay}"
Grid.Row="2"
TextWrapping="Wrap" FontSize="25" Foreground="White"
AcceptsReturn="True/>
</Grid>
我非常喜欢这种绑定方式,即通过“切换”`DataContext` 来创建 UI 中与不同对象绑定的“孤岛”。
通过将 `ItemsControl` 切换为 `ListBox`,列表现在具有指示焦点和选择的标准高亮显示。默认的浅蓝色框与此应用程序的 UI 并不匹配,因此我模板化了 `ItemsControl`,在突出显示的项旁边添加了一个简单的“点”。我能够去除模板的大部分通用绑定,但仍然剩下相当大的 XAML 代码块。
<ControlTemplate TargetType="ListBoxItem" x:Key="listBoxItemTemplate">
<Border x:Name="LayoutRoot">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
</VisualStateGroup>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Unselected">
<Storyboard>
<DoubleAnimation Duration="0" To="0"
Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="selectedDot" d:IsOptimized="True"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Selected">
<Storyboard>
<DoubleAnimation Duration="0" To="1"
Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="selectedDot" d:IsOptimized="True"/>
</Storyboard>
</VisualState>
<VisualState x:Name="SelectedUnfocused">
<Storyboard>
<DoubleAnimation Duration="0" To="1"
Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="selectedDot" d:IsOptimized="True"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<!-- the 'dot' that indicates selection -->
<Ellipse Width="15" Height="15"
VerticalAlignment="Center" HorizontalAlignment="Left"
Fill="White" x:Name="selectedDot"
Opacity="0"/>
<!-- the content for this item -->
<ContentControl x:Name="ContentContainer"
ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}"
HorizontalContentAlignment="Stretch"
Foreground="#FF1BA1E2" Margin="20 0 0 0"/>
</Grid>
</Border>
</ControlTemplate>
视觉状态仅根据选择状态显示/隐藏点。
Windows 8 应用程序可以处于两种状态:全屏或分屏。如果您没有触摸屏,可以通过按 `“Windows 键” + “.”` 进入分屏模式。
待办事项应用程序的 UI 使用了一个 `Grid`,其中包含两个“星形”宽度的列,将屏幕分为左右两个窗格。当应用程序分屏时,这可以很好地缩放,但最终结果并不太好看。
对于此应用程序,最好在分屏时定制界面。这可能适用于大多数 Windows 8 应用程序!
当应用程序分屏时,不要求它必须提供与全屏运行时相同的所有功能。与其尝试重新排列或缩小您的界面,不如考虑用户为何要分屏使用您的应用程序以及他们将执行哪些任务?
对于这个待办事项列表应用程序,我可以设想用户可能希望将项目列表分屏显示在侧面,同时使用占据屏幕大部分空间的另一个应用程序来完成列出的任务。因此,我认为分屏视图仅渲染列表是完全可以的,这样他们就可以将项目标记为完成或删除(我们稍后会添加此功能),但强制他们全屏才能添加或编辑项目。
查看 VS2012 附带的示例或网络上的示例,您可以看到当应用程序分屏时,页面的视觉状态会发生变化。
因此,我继续添加了一些视觉状态,当应用程序分屏时显示/隐藏各种元素。我选择创建第二个列表来渲染分屏视图中的项目,以便我可以更轻松地控制 `ItemTemplate` 和其他属性。
<Page ...>
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"
x:Name="ContentRoot">
<!-- application layout goes here -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ApplicationViewStates">
<VisualState x:Name="FullScreenLandscape"/>
<VisualState x:Name="Filled"/>
<VisualState x:Name="FullScreenPortrait"/>
<VisualState x:Name="Snapped">
<Storyboard>
<!-- hide the item details view -->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="itemDetailGrid" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
<!-- hide the application title -->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="leftHandTitle" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
<!-- hide the to-do list -->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="todoList" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
<!-- show a more compact version of the list -->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="todoListCompact" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>
然而,令我非常困惑的是,这并没有奏效!
在更详细地研究了示例后,我发现框架不会设置应用程序页面的视觉状态。框架提供了一个静态属性 `ApplicationView.ApplicationViewState`,它反映了当前的方向和分屏状态。您会发现 Windows 8 SDK 附带的许多示例都包含一个名为 `LayoutAwarePage` 的类,该类将此属性映射到视觉状态。
不管怎样,我该下我的讲台了。
我将 `LayoutAwarePage` 添加到我的项目中,并将 `MainPage` 修改为以它作为超类,瞧,我的分屏视图奏效了!这是分屏显示的列表,旁边是 Bing 应用程序提供的美丽乡村风光图片。
添加交互
到目前为止,视图模型已经移植,应用程序 UI 也已经为 Windows 8 定制。是时候开始移植使这个应用程序最初如此有趣的待办事项列表的手势了。
我在上一篇文章中描述的 Windows Phone 应用程序使用了 `IInteraction` 接口定义的交互概念,该接口将事件处理程序附加到列表中的每个项目。`InteractionManager` 协调各种交互,以确保一次只有一个交互处于活动状态。这些概念可以很好地移植到该应用程序的 Windows 8 版本。
我采取了与处理视图模型相同的方法,将 Windows Phone 类添加到 Windows 8 项目,然后开始解决编译问题。这次移植代码所需更改要多得多。
- 大量命名空间更改!
- 操纵事件参数不同 `ManipulationCompletedEventArgs` => `ManipulationCompletedRoutedEventArgs`(对于 Started、Delta 和其他事件也一样)。
- 事件参数不同 `e.TotalManipulation` => `e.Cumulative`
- 缺少 `e.FinalVelocities` 属性,这使得很难创建管理自己的惯性/动量的交互。
- `e.DeltaManipulation` => `e.Delta`
- 动画缓动属性的类型更改,`IEasingFunction` => `EasingFunctionBase`。
- Storyboard 目标属性是字符串类型,而不是 `PropertyPath` 实例。
- `LayoutUpdated` 事件是 `EventHandler
为了让代码编译,对代码进行的更改列表相当长。令人沮丧的是,Windows 8 和 Windows Phone 在功能方面没有根本性的区别。API 只是略有不同!
不管怎样,我已经讲过一次了,现在我先把它放在一边,继续让它工作。
通过上述更改,我能够使 `SwipeInteraction` 代码编译,并通过 `InteractionManager` 将其添加到 `MainPage.xaml`… 当我运行应用程序时,它什么都没做!
通过在论坛和 API 文档中进行一些研究,我发现 Windows 8 应用商店应用处理交互的方式与 Windows Phone 非常不同。您可以将操纵事件附加到任何 `UIElement`,但是,只有在您还为该元素设置了 `ManipulationMode` 时,它们才会触发。操纵模式枚举提供了允许您限制元素参与的交互类型的选项,例如 `TranslateX`、`TranslateY`。有趣的是,您还可以指定操纵是否应包含惯性,例如 `TranslateIntertia` 或 `RotateIntertia`,当应用惯性时,即使在用户停止操纵后,delta 事件也会继续触发。各种值可以 ORed 一起用于任何元素。
为了使 `SwipeInteraction` 工作,我设置了渲染每个待办事项的元素的 `ManipulationMode` 属性。不幸的是,这带来了一个相当不受欢迎的副作用,即列表不再滚动。`ScrollViewer` 具有“特殊”的 `ManipulationMode` `System`,它提供平滑的滚动体验,这无疑与 Windows Phone Mango 相似,其中滚动被推送到单独的线程。不幸的是,Windows 8 `ManipulationModel.System` 与 `ManipulationMode` 不是 None 的子元素不兼容。坦率地说,您无法在 `ScrollViewer` 中的任何元素上处理操纵事件而不破坏滚动。
这对我的应用程序来说是一个打击!
我提出的解决方法是在待办事项列表中的每个项目上添加小的“拇指”控件。这些是用户控件,位于每个项目左右边缘,并接受执行滑动手势所需的操纵。
<DataTemplate x:Key="toDoItemTemplate">
<Border Height="55" x:Name="todoItem">
<Grid Margin="2"
Background="{Binding Path=Color, Converter={StaticResource ColorToBrushConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- the two thumb controls -->
<local:ThumbControl Loaded="Border_Loaded"
Width="40" Height="55" ManipulationMode="All" />
<local:ThumbControl Loaded="Border_Loaded"
Width="40" Height="55" ManipulationMode="All"
Grid.Column="2"/>
<!-- the to-do item visuals -->
<TextBlock Text="{Binding Text, Mode=TwoWay}"
FontSize="25" TextTrimming="WordEllipsis"
Foreground="White"
VerticalAlignment="Center"
Grid.Column="1"/>
<Line Visibility="{Binding Path=Completed, Converter={StaticResource BoolToVisibilityConverter}}"
X1="0" Y1="0" X2="1" Y2="0"
Stretch="UniformToFill"
Stroke="White" StrokeThickness="2"
Margin="8,5,8,0"
Grid.Column="1"/>
</Grid>
</Border>
</DataTemplate>
您可以在下面的图片中看到“拇指”控件。
有了这个,应用程序就可以接受向右滑动以完成和向左滑动以删除的手势。尽管用户当然必须从其中一个“拇指”滑动,并且必须使用待办事项项的中心区域进行滚动。
通过更多的代码调整,我也能够添加允许您拖动重新排序列表的手势。这次我遇到的主要问题是 Windows 8 的 `WriteableBitmap` 没有接受 `FrameworkElement` 作为参数的构造函数。在 Windows Phone 中,这是一种非常流行的技术,可以用来“克隆”UI 的一部分,以实现一些高级图形效果。在 Windows 8 中,我不得不做出一些妥协,导致了 z-index 问题,即拖动的项目并不总是在您正在拖动的项目之上。
下图显示了一个正在拖动的项目。
Windows Phone 应用程序还有其他几个新颖的交互。第一个是下拉添加新项目交互,我发现无法在 Windows 8 中实现。这并不奇怪,Windows Phone 版本有点像一个 hack,它依赖于鼠标事件和探测 `ScrollViewer` 内容来检查框架应用的转换。第二个是捏合添加新项目的手势,我的感觉是,Windows 8 施加的 `ManipulationMode` 限制会使这个手势在技术上是可行的,但对用户来说太不舒服了。
过渡
好的,是时候报告 Windows 8 出现的一些好消息了——过渡。实际上,我换个说法,它们是一个很棒的新功能!
在 Windows Phone 上,我发现 OS 附带的应用程序比我其他 Silverlight 开发者编写的应用程序更快、更流畅,这有点令人沮丧。使用 OS 应用(电子邮件、日历等…),列表会滑动并过渡,项目会倾斜,搜索结果会流动。因此,我开始了一个名为“Metro In Motion”的七部分博客系列,其中我展示了如何模仿 OS 应用中的流畅动画。这个系列相当受欢迎!
Windows 8 将许多 Metro 风格的过渡直接内置到框架中。您可以为任何元素的 `Transitions` 属性设置值,例如 `EntranceThemeTransition`、`AddDeleteThemeTransition` 等… 元素将根据它们出现、删除等情况自动进行动画处理。
更强大的是 Panel 的 `ChildTransitions` 属性,它将指定的过渡应用于其子元素,并在每个项目添加时稍作延迟触发它们。这会产生优雅流畅的界面,无需编写任何代码。我认为这是一个很棒的新功能,并且我在 CTP 发布时详细写过。
用简单的截图很难捕捉到这些过渡,但这是应用程序的进入过渡。
我强烈鼓励您熟悉过渡并将其用于您自己的应用程序。
动态磁贴
是时候添加一些 Windows 8 特有的功能了……
虽然 Windows Phone 支持动态磁贴,但 Windows 8 将其提升到了一个新的水平。动态磁贴(和辅助磁贴)可以根据框架中的多个模板显示各种信息。更新应用程序的动态磁贴就像通过 `TileUpdateManager` 发送 XML 磁贴通知一样简单。
该文档提供了所有磁贴通知类型的全面概述及其 XML。不幸的是,SDK 示例有点过于复杂,它们在这些 XML 消息之上提供了一个抽象层,使它们更容易创建。我的感觉是,同样,SDK 示例应该简单明了。如果示例作者认为框架不够简单易用,并最终编写了抽象层,那么 SDK 本身就需要简化!
发送一个更新磁贴状态的 XML 消息并不是很难。我更新了 `ToDoListViewModel` 以检测其自身状态的变化,并使用以下代码通知动态磁贴。
private void UpdateTileStatus()
{
XmlDocument tileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileWideBlockAndText01);
int index = 0;
foreach(ToDoItem item in _todoItems.Take(4))
{
SetElementText(tileXml, index++, item.Text);
}
SetElementText(tileXml, 4, _todoItems.Where(t => !t.Completed).Count().ToString());
SetElementText(tileXml, 5, "Left to-do");
TileUpdateManager.CreateTileUpdaterForApplication().Update(new TileNotification(tileXml));
}
private void SetElementText(XmlDocument doc, int index, string text)
{
var element = (XmlElement)doc.GetElementsByTagName("text")[index];
element.AppendChild(doc.CreateTextNode(text));
}
这会生成以下格式的 XML。
<tile>
<visual>
<binding template="TileWideBlockAndText01">
<text id="1">Feed the cat</text>
<text id="2">Buy eggs</text>
<text id="3">Pack bags for the MVP conference</text>
<text id="4">Rule the web</text>
<text id="5">18</text>
<text id="6">Left to-do</text>
</binding>
</visual>
</tile>
我发现的唯一小问题是模拟器不支持动态磁贴,要测试上述代码,您必须在“本地计算机”上运行该应用程序。除此之外,动态磁贴非常易于使用。下面的屏幕截图显示了应用程序的动态磁贴,它列出了待办事项列表中的前四个项目以及剩余要完成的项目数。

搜索协议
protected override void OnWindowCreated(WindowCreatedEventArgs args)
{
// Register QuerySubmitted handler for the window at window creation time and only registered once
// so that the app can receive user queries at any time.
SearchPane.GetForCurrentView().QuerySubmitted += OnQuerySubmitted;
}
在待办事项列表应用程序中,我在应用程序级别处理了此问题,通过静态单例(天哪!)将搜索输入发送到 MainPage。
public ObservableCollectionEx<ToDoItem> Items
{
get
{
if (string.IsNullOrEmpty(_searchText))
{
return _todoItems;
}
else
{
return new ObservableCollectionEx<ToDoItem>(
_todoItems.Where(i => i.Text.ToLowerInvariant().Contains(_searchText.ToLowerInvariant())));
}
}
}
我很高兴将我的手势驱动待办事项列表应用程序移植到 Windows 8,并且在此过程中经历了一段有趣的旅程。Windows 8 和 Windows Phone 之间有许多细微的差异,这可能会导致沮丧,或者更糟糕的是,有些功能根本无法移植或实现。另一方面,Windows 8 也有一些非常棒的新功能,如动态磁贴、过渡、分屏视图等。此外,Windows 8 模拟器非常出色,为没有触摸屏的用户提供了多点触控模拟,这是我作为 Windows Phone 开发者非常怀念的东西。
Windows 8 并不完美,作为一名经验丰富的 WPF、Silverlight 和 Windows Phone 开发人员,它并没有真正感觉像是一个巨大的进步……更像是一个横向移动。然而,我刚刚在笔记本电脑上安装了 Windows 8,我惊叹于 Windows 团队如何在 Windows 8 中创建如此根本不同的操作系统,但仍然可以使用 VS 2010 Express 在桌面模式下开发 Windows Phone 应用程序。
Windows 8 与之前的 Windows 版本有着根本性的区别。希望在未来一年左右的时间里,平台会成熟,小瑕疵也会得到解决。
您可以下载此应用程序的完整源代码: Clearer.zip