在C#/WPF/Silverlight中完成数独游戏(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/complete-sudoku-game-in-csharp-wpf-silverlight-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 60 分钟阅读 - 29958 个词 阅读量 0在C#/WPF/Silverlight中完成数独游戏(译文)
原文地址:https://www.codeproject.com/Articles/855699/Complete-Sudoku-Game-in-Csharp-WPF-Silverlight
原文作者:Hung, Han
译文由本站 robot-v1.0 翻译
前言
This article is about converting a VB.NET WinForms application to C# using WPF and Silverlight.
本文是有关使用WPF和Silverlight将VB.NET WinForms应用程序转换为C#的.
- 下载SudokuWPF-73.7 KB(Download SudokuWPF - 73.7 KB)
- 下载Sudoku_Silverlight-312.2 KB(Download Sudoku_Silverlight - 312.2 KB)
介绍(Introduction)
写完后(After writing a) 数独游戏(Sudoku game) 使用VB.NET和WinForms,我决定将其翻译为C#.为了使事情变得有趣,我决定使用WPF而不是WinForms创建UI层.而且由于Silverlight是WPF的子集,所以我想看看将完成的WPF代码也转换为Silverlight多么容易.(using VB.NET and WinForms, I decided to translate it to C#. To make things interesting, I decided to create the UI layer using WPF instead of WinForms. And since Silverlight is a subset of WPF, I wanted to see how easy it was to convert the finished WPF code to Silverlight as well.) 本文介绍了我遇到的问题以及我提出的解决方案和变通办法.有关游戏的背景信息以及我对游戏的一些决定以及诸如Singletons,Events等使用的编程概念的信息,请阅读原始文章:(This article covers the issues I encountered and the solutions and work-arounds that I came up with. For background information on the game and some of the decisions I made regarding game play as well as the programming concepts used like Singletons, Events, etc., please read the original article:) 使用VB.Net 2013完整的Windows Sudoku游戏(Complete Sudoku Game for Windows using VB.Net 2013) .(.) 本文可用作WPF和Silverlight编程的简介.我将假定读者具有有关C#和WinForms的一些知识.对于那些已经了解WPF和Silverlight的人,这篇文章会很无聊.但是请随时阅读,下载代码并使用它.我们欢迎任何改进建议或其他替代方法.(This article can be used as an introduction to programming in WPF and Silverlight. I will assume that the reader has some knowledge about C# and WinForms. For those who already know WPF and Silverlight, this article will be pretty boring. But feel free to read it, download the code and play with it. Any suggestions for improvement or alternative ways to do something are most welcome.)
背景(Background)
Windows Presentation Foundation或WPF于2006年11月首次与.NET 3.0一起发布.WPF本质上是一种呈现引擎,具有许多强大的功能,使开发人员可以为独立的,由浏览器托管的和电话应用程序.在构建WPF前端时,人们使用XAML或可扩展应用程序标记语言来描述UI.然后WPF引擎将解释XAML并将UI输出到Windows应用程序,浏览器或Windows Store/Phone App,具体取决于项目.这是一个简单的图表,显示了我的意思.(Windows Presentation Foundation, or WPF, was first released with .NET 3.0 back in November of 2006. WPF is essentially a rendering engine with lots of powerful features to allow a developer to create visually stunning client applications for both standalone, browser-hosted, and phone applications. When building the WPF front-end, one uses XAML, or Extensible Application Mark-up Language, to describe the UI. The WPF engine will then interpret the XAML and output the UI to either a Windows application, browser, or Windows Store/Phone App depending on the project. Here is a simple diagram that shows what I mean.)
实际上,Silverlight和Windows Store App是WPF的子集.因此,当作为Silverlight应用程序输出到浏览器或Windows Phone时,与Windows应用程序相同的XAML可能无法正常工作.(In reality, Silverlight and Windows Store App is a subset of WPF. So, the same XAML that works for a Windows Application may not work when outputting to a browser as a Silverlight app or to a Windows Phone.) 在WPF中构建UI时,不是直接在设计器窗口的"属性"页面上设置控件的属性,而是直接使用XAML代码窗口来设置属性.无论是在VB.NET还是C#中,这都与编写WinForms应用程序完全不同.如果输入XAML代码令人生畏,IDE还将提供一个"属性"工具窗口,您也可以在其中修改属性.开始编写WPF应用程序时,最好使用"属性"工具窗口,以便熟悉所有可用的属性.(When building the UI in WPF, instead of setting properties for the controls on the Property page of the designer window, one sets properties directly using the XAML code window. This is completely different from writing a WinForms application, whether it is in VB.NET or C#. If entering XAML code is intimidating, the IDE also has a Properties tool window where one can modify the properties as well. When starting out writing WPF applications, it might be better to use the Properties tool window so that one can get familiar with all the properties that are available.) 有很多教程,示例代码和在线帮助可以帮助您开始使用WPF.这是微软自己的链接(There are lots of tutorials, sample code, and help on-line to get you started with WPF. Here is a link to Microsoft’s own) 网站(website) 在WPF上:(on WPF:) WPF简介(Introduction to WPF) .在本文中,我将介绍WPF与编写游戏有关的一些要点.(. In this article, I’ll go over some of the more salient points of WPF as it relates to writing the game.) 对于该项目,我没有遵循在另一个项目中使用的MVC编程模式,而是尝试遵循MVVM或Model-View-View Model模式,这是编写WPF应用程序时推荐使用的模式.这是从Microsoft网站获取的图表,其中详细介绍了MVVM模式的不同部分之间的流程.请注意额外的"(For this project, instead of using the MVC programming pattern like I did in the other project, I tried to adhere to the MVVM, or Model-View-View Model pattern, which is the recommended pattern to use when writing WPF applications. Here is a diagram taken from Microsoft’s website that details the flow between the different parts of the MVVM pattern. Note the extra “)数据绑定(Data Binding)与MVC模式中的视图和视图模型之间的链接”.(" link between the View and the View Model compared to that in the MVC pattern.)
在MVC模式中,业务逻辑通常保留在Controller中.在MVVM模式中,业务逻辑被下推到模型层.由于原始代码是使用MVC模式编写的,因此我将大多数业务逻辑保留在VM层中.在线上有很多文章更详细地描述了这种编程模式,以及MVC和MVVM编程模式之间的差异.(In the MVC pattern, the business logic is generally kept in the Controller. In the MVVM pattern, the business logic is pushed down to the Model layer. Since the original code was written using the MVC pattern, I kept most of the business logic in the VM layer. There are lots of articles on-line that describe this programming pattern in greater detail, as well as the differences between the MVC and MVVM programming patterns.) 该代码也以MVVM模式组织.我创建了三个文件夹,并将它们分别命名为Model,View和View Model,并相应地拆分了代码.(The code was organized in the MVVM pattern as well. I created three folders and named them Model, View, and View Model and the code was split up accordingly.)
使用WPF进行编程并使用MVVM编程模式非常适合将UI设计与业务逻辑分离,并允许专家组构建复杂应用程序的不同部分.图形设计师可以使用Expression Blend甚至Visual Studio之类的工具来构建外观丰富的UI,而无需了解如何编写C#或VB代码的一行.然后,开发人员可以专注于编写业务逻辑和数据层,而不必考虑如何呈现数据.显然,应该就两个团队之间需要什么样的数据达成协议.(Programming in WPF and using the MVVM programming pattern lends itself well to separating the UI design from the business logic and allow groups of specialists to build the different parts of complex applications. Graphic designers can use tools like Expression Blend or even Visual Studio to build a visually rich UI without having to understand how to write a single line of C# or VB code. The developer can then concentrate on writing the business logic and data layers without having to think about how the data will be presented. Obviously, there should be an agreement as to what kind of data is required between the two teams.) 我觉得了解和掌握WPF的关键之一就是了解数据绑定. WPF数据绑定允许将UI元素(如文本框)绑定到VM层中的属性.如果操作正确,当属性更改时,UI也将自动更改.这是到Microsoft网站的链接,该链接解释了(I feel that one of the keys to understanding and mastering WPF is understanding Data Binding. WPF Data Binding allows UI elements, like text boxes, to be bound to a property in the VM layer. And if done right, when the property changes, the UI will automatically change as well. Here is a link to Microsoft’s website that explains) 数据绑定(data binding) 以及示例.当然,可以像在WinForms过去的美好时光一样,直接在代码背后处理控件.但这与WPF的宗旨背道而驰,比数据绑定需要更多的代码.(as well as examples. Sure, one can address the controls directly in the code-behind like we did in the good old days of WinForms. But that goes counter to what WPF is all about and requires more code behind than data binding.)
使用代码(Using the Code)
这些程序是使用VS 2013和4.5 .NET Framework/Silverlight 5编写的.它们是完整的代码.我已经在上面包括了WPF和Silverlight项目.您应该能够分别下载和编译两个项目.(The programs were written using VS 2013 and the 4.5 .NET framework/Silverlight 5. They are code complete. I have included both the WPF and the Silverlight projects above. You should be able to download and compile both projects separately.) 通常,当一个人在两个或多个项目之间共享代码时,一个人会将文件从一个项目复制到另一个项目,或将文件从第一个项目添加到第二个项目.由于同一文件有多个副本,因此这使得维护代码变得更加困难.(Normally, when one shares code between two or more projects, one would copy the file from one project to the other or add the file to the second project from the first project. This makes maintaining the code much harder since there are multiple copies of the same file.) 从VS 2010开始,您可以通过点击"(Starting with VS 2010, one can add a link to the original code file by clicking the down arrow on the “)添加现有项目(Add Existing Item)” 对话框.(" dialog box.)
但是,对于这两个项目,我没有使用此功能.因此,每个ZIP文件都包含所有必需的文件,并且一个文件可以下载您感兴趣的任何项目,而无需下载另一个文件.(However, for these two projects, I did not use this feature. So each ZIP file contains all the necessary files and one can download whichever project that interests you without having to download the other.)
从VB.NET转换为C#(Converting from VB.NET to C#)
除了语法上的差异,将代码从VB.NET移植到C#非常简单,因为两者都运行在同一CLR之上.(Syntactical differences aside, porting the code from VB.NET to C# was pretty straightforward since both run on top of the same CLR.) 除了将代码从VB.NET移植到C#之外,我还对数组的寻址方式进行了一些更改.在我的VB代码中,使用索引从1到9来寻址数组.之所以这样做,是因为游戏的有效答案是从1到9的数字.但是,在C#代码中,由于C是从零开始的语言,所以我决定遵循并将所有内容都更改为零.有效答案仍然是1到9.花费了一段时间,但并不难.(In addition to porting the code from VB.NET to C#, I also made some changes to the way arrays were addressed. In my VB code, arrays were addressed using the indices from 1 through 9. I did that because the valid answers for the game are the numbers from 1 through 9. However, in the C# code, since C is a zero based language, I decided to conform and changed everything to be zero based. Valid answers are still 1 through 9 though. That took a little while, but it was not hard to do.)
将UI从WinForms转换为WPF(Converting the UI from WinForms to WPF)
将代码移植到C#之后,下一步是将UI从WinForms转换为WPF.这需要更多的努力.首先要做出的决定是(After porting the code over to C#, the next step was to convert the UI from WinForms to WPF. That took a little more effort. The first decision to make was what kind of) Panel
用作基础的对象.在WinForms中,只有一个空白表单,然后开始在表单上放置控件以构建UI.但是在WPF中,有几种不同的背景或(object to use as the base. In WinForms, there is just a blank form and one starts putting controls on the form to build the UI. But in WPF, there are several different kinds of background or) Panel
用于构建UI的对象,每个对象都有不同的特征.因此,为项目选择合适的项目对于整体成功至关重要.这是一个链接到(objects to build the UI with and each one has different characteristics. So choosing the right one for the project is essential to the overall success. Here is a link to a) 网站(website) 描述差异.默认为(that describes the differences. The default is the) Grid
.(.)
既然如此,我可能想在后台实现烟火表演,因此我决定使用(Since down the line, I might want to implement a fireworks display in the background, I decided to use the) Canvas
作为我主窗口的背景.的(as my main window’s background. The) Canvas
面板允许我直接在控件上绘制.然后,我添加了所有必需的控件,以使其看起来像原始的VB版本.(Panel allows me to draw directly on the control. I then added all the necessary controls to make it look like the original VB version.)
WPF和WinForms都有按钮,组合框,标签和复选框.因此,这很容易复制.(Both WPF and WinForms have buttons, comboboxes, labels, and checkboxes. So that was easy to replicate.)
另一方面,游戏网格是一个挑战,因为WPF没有类似的功能(The game grid on the other hand was a challenge since WPF does not have a similar) TableLayoutPanel
控制.另一方面,WPF有两种网格:(control. On the other hand, WPF has two kinds of grids: a) DataGrid
和一个(and a) Grid
格.我觉得(grid. I felt that the) DataGrid
更多用于以表格格式显示数据项的滚动列表.由于我对滚动不感兴趣并且数据大小是静态的,因此我选择使用(was more for displaying a scrolling list of data items in a tabular format. Since I am not interested in scrolling around and the data size is static, I opted to use a) Grid
控制.(control instead.)
我加了(I added a) Grid
控制到我的表单,然后将其分为3行和3列.在每个单元格中,我添加了另一个(control to my form and then split it into 3 rows and 3 columns. Within each cell, I added another) Grid
控制并将其分为3行和3列.然后,我添加了边框和矩形,并更改了背景色以构建游戏网格.由于没有线对象,因此我使用高度(或宽度)为一个像素的矩形来表示游戏网格上的线.(control and also split it into 3 rows and 3 columns. I then added borders and rectangles as well as changed the background color to build the game grid. Since there is no line object, I used rectangles with a height (or width) of one pixel to represent the lines on the game grid.)
下一个问题是底部的状态栏. WPF也有一个(The next issue was the status bar at the bottom. WPF also has a) StatusBar
控件,但其工作方式与WinForms不同(control, but it works differently than the WinForms) StatusBar
.与其使用Items Collection Editor将项目添加到状态栏中,不如直接在XAML代码中添加项目.(. Instead of using the Items Collection Editor to add items to the Status Bar, one adds items directly in the XAML code.)
这是WPF中主窗口的最终版本:(Here is the final rendition of the main window in WPF:)
您会看到与原始VB版本不同的几件事是(A couple of things that you will see that are different from the original VB version is that the)暗示(Hint)和(and)明确(Clear)按钮丢失.我将这些按钮移至"输入板"对话框.我这样做的原因是(buttons are missing. I moved those buttons to the Input Pad dialog. The reason I did that was because)暗示(Hint)和(and)明确(Clear)是针对特定单元的操作,而不是针对特定游戏的操作.因此,它们确实属于Input Pad窗口,其中包含特定于单元的动作.人们可以对"(are more cell specific actions rather than game specific. And therefore, they really belong to the Input Pad window which contain cell specific actions. One could make the same argument for the “)输入注释(Enter Notes)“复选框.有时,您想输入多个单元格的注释,而不仅仅是单个单元格.这就是为什么我将其留在主窗口中的原因.(” checkbox as well. But there are times when one wants to enter notes for multiple cells and not just for a single cell. That is why I left it on the main window.)
下一步是为按钮连接事件操作.在按钮的XAML部分中,我仅指定了(The next step was to wire up the event action for the buttons. In the XAML section for the button, I just specified the) Click
事件作为XML属性:(event as an XML property:)
<Button Content="Close"
Style="{StaticResource ButtonBaseStyle}"
Margin="430,411,0,0"
Click="btnClose_Click"/>
后面的代码看起来像这样,类似于(And the code-behind looks like this, which looks similar to a) WinForms
事件:(event:)
private void btnClose_Click(object sender, RoutedEventArgs e)
{
}
我暂时将代码隐藏的细节留空了.准备好在View模型上工作后,我将添加代码以将它们连接在一起.(I left the details of the code-behind blank for now. Once I am ready to work on the View Model, I will add code to wire them both together.)
WPF和数据绑定(WPF and Data Binding)
为每个按钮连接动作或事件代码后,下一步是将UI连接到数据.注意,我完全没有提到复选框.这是因为我们可以使用WPF数据绑定来设置复选框并执行其他非常酷的事情.我将在后面解释.(After wiring up the action or event code for each of the buttons, the next step was to wire up the UI to the data. Notice, I did not mention checkboxes at all. This is because we can use WPF data binding to set the checkboxes and do other really cool things. I will explain more later on.) 我本可以很容易地遵循WinForms约定,而只是将数据分配给表单中的代码,如下所示:(I could have easily followed the WinForms conventions and just assigned the data to the control in the form’s code-behind like so:)
this.ElapsedTime.Content = "00:00:00";
由于WPF具有将UI连接到数据的强大工具,因此我将使用它.(Since WPF has powerful tools to connect the UI to the data, I will use that instead.)
它以(It starts with the) ViewModel
类并添加(class and adding the) INotifyPropertyChanged
界面,它是(interface, which is part of the) System.ComponentModel
命名空间.(namespace.)
using System.ComponentModel;
internal class ViewModelClass : INotifyPropertyChanged
其他相关声明如下.我声明该事件,然后编写代码以引发该事件.(The other related declarations are as follows. I declare the event and then write code to raise the event.)
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
这是它的用法:(And this is how it is used:)
public string GameCountVeryEasy
{
get
{
return GetGameCount(DifficultyLevels.VeryEasy);
}
private set
{
OnPropertyChanged();
}
}
的(The) set
访问器只有一行代码,因为我们不需要将实际值保存在(accessor has only one line of code since we do not need to save the actual value in the) ViewModel
类.当我们需要它时,(class. When we need it, the) get
访问者拨打了电话(accessor makes a call to) GetGameCount
它的存储位置.(where it is stored.)
然后在XAML中,其调用方式如下:(Then in the XAML, here is how it is called:)
<Label Content="{Binding Path=GameCountVeryEasy, Mode=OneWay}"/>
容易吧?实际上,我还缺少其他一些东西.首先,在XAML标头声明中,我需要指定名称空间,其中属性(Easy, right? Actually, I am missing a few more things. First, in the XAML header declaration, I need to specify the namespace where the property) GameCountVeryEasy
位于.为此,我需要将以下行添加到(is located. To do this, I need to add the following line to the) <Window
标记顶部:(tag up top:)
xmlns:srcVM="clr-namespace:SudokuWPF.ViewModel"
然后,在表单的代码隐藏中,我需要添加以下行:(Then, in the form’s code-behind, I need to add the following line:)
this.DataContext = _viewModel;
通常,在初始化View Model类时将其添加到窗体的构造函数中,紧随其后(Normally, it is added when the View Model class is initialized, either in the constructor of the form, right after) InitializeComponent()
,或接近该位置的地方.这告诉UI哪个View Model类实例具有实际数据.有趣的是,我不必在XAML代码中指定找到该属性的类的实际名称.那就是(, or somewhere close to that. This tells the UI which instance of the View Model class has the actual data. Interestingly enough, I did not have to specify in the XAML code, the actual name of the class where this property is found. That is what the) DataContext
是为了.它告诉WPF在哪里可以找到绑定到UI控件的属性.(is for. It tells WPF where to find the properties that are bound to the controls on the UI.)
让我更详细地解释上面代码中的一些更有趣的观点.首先,(Let me explain in more details some of the more interesting points in the code above. First, the) [CallerMemberName]
中的属性(attribute in the) OnPropertyChanged
方法的参数.(method’s parameter.)
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
此属性允许(This attribute allows the) OnPropertyChanged
method获取方法或该方法的调用者的属性名称.这是.NET 4.5的新功能,是(method to obtain the method or property name of the caller to the method. This is new for .NET 4.5 and is part of the) System.Runtime.CompilerServices
命名空间.这是微软的链接(namespace. Here is a link to Microsoft’s) 网站(website) 详细描述了此属性.(that describes this attribute in more details.)
C#的另一个新概念是创建可选参数的能力.这是VB世界多年的经验.因此,对于那些已经熟悉VB可选参数的人,上面的语法已经很熟悉了.唯一的区别是没有(Another concept new to C# is the ability to create optional arguments. This is something that the VB world has had for years. So for those who are already familiar with VB’s optional arguments, the syntax above is already familiar. The only difference is that there is no) Optional
限定词.简而言之,当我们添加(qualifier. Simply put, when we add) = ""
到参数声明的末尾,它将那个参数变成一个可选参数.如果调用中省略了参数,则空白字符串仅表示这是默认值.任何有效(to the end of a parameter declaration, it turns that parameter into an optional parameter. The blank string simply means that is the default value if the parameter were omitted from the call. Any valid) string
可以用作默认值.有关可选参数的更多信息,请访问Microsoft的链接.(can be used as the default. For more information about optional parameters, here is the link to Microsoft’s) 网站(website) .(.)
通常,当一个电话(Normally, when one calls) OnPropertyChanged
,必须传递属性的名称,以便绑定到该属性的相应WPF元素将知道它已更改.例如:(, one has to pass the name of the property so that the corresponding WPF element that is bound to that property will know that it changed. For example:)
OnPropertyChanged("GameCountVeryEasy");
通过使用(By using the) [CallerMemberName]
属性和可选参数(如果调用来自实际属性)(attribute and optional parameters, if the call came from the actual property) GameCountVeryEasy
,我们可以省略(, we can omit the) propertyName
呼叫的参数,因为(parameter from the call since) [CallerMemberName]
会自动为我们填写.(will fill it in for us automatically.)
在C#中,名称空间使用(In C#, namespaces are declared with the) using
代码页开头的关键字. XAML的使用(keyword at the beginning of the code page. XAML uses) xmlns:
.在以下XAML名称空间声明中:(. In the following XAML namespace declaration:)
xmlns:srcVM="clr-namespace:SudokuWPF.ViewModel"
这就是一切.(Here is what it all means.) xmlns:
只是允许我们指定名称空间的XML属性.(is just the XML attribute that allows us to specify a namespace.) srcVM
是一个快捷方式名称,如果我们需要引用该命名空间中的类,属性或方法,则可以在XAML代码中使用.我可以说(is a shortcut name that can be used in the XAML code if we need to refer to a class, property, or method in that namespace. I could have said) viewModel, vm,
或与此相关的任何有效名称.最后,内容(or any valid name for that matter. And finally, the content of the) string
是实际名称空间的名称. XAML编辑器会自动添加(is the name of the actual namespace. The XAML editor automatically adds) "clr-namespace:"
输入最终的双引号时.输入第一引号时,intellisense会在下拉列表中填充当前项目中的所有有效名称空间.(when the final double quotation is entered. When you enter the first quotation, intellisense populates a dropdown list with all the valid namespaces in the current project.)
这是WinForms和WPF之间的另一个巨大区别.在WPF中,不必命名表单上的所有元素.所以我们的标签控件只有一个(Here is another huge difference between WinForms and WPF. In WPF, it is not necessary to name all the elements on a form. So our label control just has a) Content
属性.绑定代码是将该元素与数据源联系起来的方式,这就是我们将如何从代码中更新该元素的方式.(property. The binding code is what ties this element to the data source and that is how we will update that element from code.)
<Label Content="{Binding Path=GameCountVeryEasy, Mode=OneWay}"/>
一般而言,如果不需要从代码中访问控件,则无需命名.数据绑定就足够了.(Generally speaking, if there is no need to address the control from code, then there is no need to give it a name. The data binding is sufficient.) Binding
是一个关键字,它告诉XAML解释器遵循绑定代码.(is a key word that tells the XAML interpreter that binding code follows.) Path=
指定要使用的属性,由(specifies the property to use specified by the) DataContext
.(.) Mode=OneWay
指定绑定从视图模型到视图,或者换句话说,到只读属性.如果未指定,则绑定默认为(specifies that the binding goes from the View Model to the View, or in other words, to a read-only property. If it is not specified, then the binding defaults to) TwoWay
.这是微软的链接(. Here is a link to Microsoft’s) 网站(website) 在WPF中进行数据绑定.(that goes over data binding in WPF.)
因此,使用上面所有的接线,当我们想使用新值更新标签时(So, with all the wiring above, when we want to update the label with a new value for) GameCountVeryEasy
,我们需要做的就是在(, all we need to do is make the following call in the) ViewModel
类.(class.)
GameCountVeryEasy = "4";
接下来会发生的是,在酒店的(What happens next is, in the property’s) set
存取器(accessor, the) OnPropertyChanged
方法被调用并且(method is called and the) PropertyChanged
事件引发. WPF引擎,正在监听所有(event is raised. The WPF engine, which is listening to all) PropertyChanged
事件,被触发.然后,它寻找相应的数据绑定.找到后,WPF然后调用该属性的(events, is triggered. It then looks for the corresponding data binding. Once it is found, WPF then calls the property’s) get
访问器并为其分配标签(accessor and assigns the label the) string
的价值(value of) 4
.(.)
绑定到的属性几乎可以位于项目中的任何类中.它并不仅限于(The property to bind to can be located in pretty much any class in the project. It does not have to be limited to) ViewModel
类.实际上,我将游戏单元绑定到了(class. In fact, I bind the game cells to properties that are found in the) Model
课.您只需要添加(class as well. All you need is to add the) INotifyPropertyChanged
界面和相关代码.然后将其绑定到您的XAML中.(interface and the related code. Then just bind to it in your XAML.)
由于以下是WinForms等效项,因此它似乎并不令人印象深刻.在WinForm的后台代码中,我们将编写以下代码:(It may not seem impressive since the following is the WinForms equivalent. In a WinForm’s code-behind, we would write the following code:)
private delegate void SetGameCountCallback(Int32 value);
internal void SetLabel(Int32 value)
{
if (label1.InvokeRequired)
{
SetGameCountCallback callback = new SetGameCountCallback(SetLabel);
this.Invoke(callback, new object[] { value });
}
else
label1.Text = value.ToString();
}
和中的代码(And the code in the) set
的访问者(accessor of the) GameCountVeryEasy
属性就是:(property would simply be:)
Form1.SetLabel(value);
而且我们不需要(And we would not need a) get
访问器,因为我们已经将值分配给了表单上的标签.(accessor since we already assigned the value to the label on the form.)
从表面上看,WinForms看起来比WPF等效要简单得多,但是WPF数据绑定的作用远不止简单地设置标签的内容.同样,我们必须为每个要更新的控件复制此代码.自然,我们可以编写此方法的通用版本,在其中传递需要更新的控件.(On the surface, WinForms looks much simpler than the WPF equivalent, but WPF data binding can do so much more than simply setting the contents of a label. Also, we would have to replicate this code for each and every control that we want to update. Naturally, we can write a generic version of this method where we pass in the control that needs to be updated.)
在WinForms中,我们需要调用(In WinForms, we need to call the) InvokeRequired
标签的方法,以确保我们正在进行线程安全的调用.在WPF中,我们不需要检查调用是否是线程安全的. WPF为我们处理所有这些工作.所以我们要做的就是提高(method of the label to make sure that we are making a thread-safe call. In WPF, we do not need to check if the call is thread safe or not. WPF takes care of all that for us. So all we need to do is just raise the) PropertyChanged
事件,WPF负责其余的工作.我还应该提到(event and WPF takes care of the rest. I should also mention that the) OnPropertyChanged
可以从类中的任何地方调用该方法.只要将正确的属性名称传递给该方法,它就会被更新.我在项目中使用了这种技术,稍后我将讨论它.(method can be called from anywhere in the class. So long as the correct property name is passed to the method, it will get updated. I used this technique in my project and I will talk about it later on.)
数据绑定和复选框(Data Binding and Checkboxes)
这是WPF数据绑定的真正魔力.对于复选框,我们还将在(Here is the real magic of WPF data binding. For the check boxes, we also create properties in the) ViewModel
用于存储复选框状态的类.例如,对于(class to store the checkbox state. For example, for the) EnterNotes
复选框,我们在(checkbox, we have the following property definition in the) ViewModel
类:(class:)
public bool IsEnterNotes
{
get
{
return _isEnterNotes;
}
set
{
_isEnterNotes = value;
OnPropertyChanged();
}
}
该属性很容易说明.我们返回或保存复选框的状态.保存状态时,我们也称(The property is pretty self explanatory. We either return or save the state of the check box. When we save the state, we also call the) OnPropertyChanged
提高方法(method to raise the) PropertyChanged
事件.(event.)
复选框XAML声明如下所示:(The check box XAML declaration looks like this:)
<CheckBox Style="{StaticResource CheckboxBaseStyle}"
IsChecked="{Binding Path=IsEnterNotes}"
Content="Enter Notes"
Margin="23,420,0,0" />
的(The) IsChecked
属性绑定到(property is bound to the) IsEnterNotes
的属性(property of the) ViewModel
.并且由于我们想知道用户何时单击复选框,因此我们希望绑定是双向的.所以绑定模式使用默认(. And since we want to know when the user clicks the check box, we want the binding to go both ways. So the binding mode uses the default) TwoWay
.因为(. Because the) IsChecked
复选框的属性是绑定的,我们不需要连接(property of the check box is bound, we do not need to wire up the) Click
事件在代码背后.那是少担心的一件事.但是,任何(event in the code-behind. That is one less thing to worry about. However, any) Click
我们以前在WinForms版本中拥有的事件代码将进入(event code-behind we used to have in the WinForms version will go into the) set
的访问者(accessor of the) IsEnterNotes
属性.(property instead.)
WPF样式(WPF Styles)
WPF的强大功能之一是能够为一组控件(如按钮)创建样式.所以我们可以创建一个(One of the powerful features in WPF is the ability to create styles for a group of controls like buttons. So we can create a) `` 像这样:(like this:)
<Style x:Key="ButtonBaseStyle"
TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Width" Value="140" />
<Setter Property="Height" Value="30" />
</Style>
并将其应用于所有按钮,以使它们看起来都相同:(And apply it to all the buttons so that they all look the same:)
<Button Content="New Game"
Style="{StaticResource ButtonBaseStyle}"
Margin="430,70,0,0"
Click="btnNew_Click"/>
<Button Content="About Sudoku WPF"
Style="{StaticResource ButtonBaseStyle}"
Margin="430,376,0,0"
Click="btnAbout_Click"/>
<Button Content="Close"
Style="{StaticResource ButtonBaseStyle}"
Margin="430,411,0,0"
Click="btnClose_Click"/>
的(The) <Style></Style>
声明可以放在项目中的多个位置.它可以位于单独的资源文件中,以便用户可以选择不同的主题.它可以放在容器的资源部分中,或者在我的情况下,放在容器上方的资源部分中.所以,就在我的上方(declaration can be put in several places in the project. It can be located in a separate resource file so that the user can select different themes. It can be put in the container’s resource section, or in my case, in the resource section just above the container. So, right above my) <Canvas></Canvas>
标签,我添加了(tags, I added the) <Window.Resources></Windows.Resources>
标签并在其中插入我的样式.如果样式放在(tags and inserted my styles there. If the style is put in the) <Application.Resources></Application.Resources>
部分或单独的资源文件,范围将扩展到整个应用程序.但自从我把它放在(section or a separate resources file, the scope expands to the entire application. But since I put it in the)<Window.Resources> </Windows.Resources>(<Window.Resources></Windows.Resources>)我正在使用的窗口部分,范围仅限于该窗口.(section of the Window that I am using it, the scope is limited to that window.)
再次注意,这些按钮均未命名.(Notice again that none of these buttons are named.)
能够创建样式的好处在于,我可以对窗口上的所有按钮使用相同的样式.如果需要更改某些内容,只需更改样式即可,所有按钮都会反映出更改.在WinForms中,如果需要更改某些内容,则需要选择所有按钮,然后更改属性.(The nice thing about being able to create styles is that I can use the same style for all the buttons on the window. If I need to change something, I can just change the style and all the buttons will reflect the changes. In WinForms, if I need to change something, I need to select all the buttons and then change the property.)
如果您在样式XAML中注意到,则添加了一个(If you notice in my Style XAML, I added an) x:key
财产(property to the) `` .如果我省略了该属性,则此样式将适用于所有按钮,并且可以从按钮声明中删除以下行.(. If I omitted that property, this style will apply to all buttons and I can remove the following line from the button declaration.)
Style="{StaticResource ButtonBaseStyle}"
但是,由于我需要对两者应用不同的样式(However, since I need to apply a different style to both the)重启(Reset)和(and the)开始(Start)按钮,我添加了(buttons, I added the) x:Key
属性.原因是,这两个按钮都具有一些特殊的状态,具体取决于用户在游戏中所处的位置.(property. The reason is, both these buttons have some special states depending on where in the game play the user is in. For example, the)开始(Start)按钮,如果用户刚刚加载了新游戏,该按钮将显示”(button, if the user just loaded a new game, the button will say “)开始游戏(Start Game)".当游戏进行中时,它将更改为”(". And when the game is in progress, it will change to “)暂停(Pause)等等.与(”, etc. Same with the)重启(Reset)按钮.如果加载了新游戏或暂停了游戏,则(button. If a new game is loaded or when the game is paused, the)重启(Reset)按钮将被禁用,依此类推.(button will be disabled, and so on.)
要对这些状态进行编码,我们可以将代码添加到表单的隐藏代码中.但这将绕过WPF的另一个重要功能.(To code these states, we can add code to the form’s code-behind. But then, that would bypass another great feature of WPF.)
WPF样式触发器(WPF Style Triggers)
在上一节中,我提到了(In the previous section, I mentioned that the)开始(Start)和(and)重启(Reset)按钮会根据用户在游戏中所处的位置更改状态,并且我们可以使用WPF管理按钮的状态.为此,我们使用(buttons change state depending on where in the game play the user is in, and that we can manage the state of the buttons using WPF. To do this, we use) Style Triggers
.这是一个例子:(. Here is an example:)
<Style x:Key="EnableGameButtonStyle"
TargetType="Button"
BasedOn="{StaticResource ButtonBaseStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsEnableGameControls, Mode=OneWay}" Value="True">
<Setter Property="IsEnabled" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsEnableGameControls, Mode=OneWay}" Value="False">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
我在View Model类中添加了以下boolean属性:(I added the following boolean property to the View Model class:)
public bool IsEnableGameControls
{
get
{
return _isEnableGameControls;
}
private set
{
_isEnableGameControls = value;
OnPropertyChanged();
}
}
注意在(Notice in the) set
访问者,我打电话(accessor, I call) OnPropertyChanged
.(.)
然后,我基于(Then, I create a new Style which is based on the) ButtonBaseStyle
从上面开始,因为我希望这些按钮保持相同的外观.然后根据该属性的可能值添加了两个触发器:(from above, since I want these buttons to maintain the same look. I then added two triggers based on possible values of this property:) True
和(and) False
.基本上,如果属性是(. Basically, if the property is) true
,然后启用按钮.如果是(, then the button is enabled. If it is) false
,然后禁用该按钮.(, then disable the button.)
让我们逐行浏览此样式.我加了(Let us go through this style line by line. I added an) x:Key
属性的样式,以便我可以从(property to the style so that I can reference it from the)重启(Reset)按钮.由于此样式将用于按钮,因此我添加了(button. Since this style will be used for buttons, I added the) TargetType
属性.然后,我添加了(property. I then added the) BasedOn
财产,因为我想要(property because I want the)重启(Reset)按钮看起来像表单上的所有其他按钮.然后,我添加了(button to look like all the other buttons on the form. I then added the) Triggers
.语法非常简单.我需要将触发器绑定到View Model类中的属性,然后设置要查找的触发器的值.在该触发标记中,我设置了属性,或者当属性的值是(. The syntax is pretty straight forward. I need to bind the trigger to a property in the View Model class and then set the value for the trigger to look for. And within that trigger tag, I set the property, or properties of the button that I want to change when the value of the property is either) True
要么(or) False
.在这种情况下(. In the case of the)重启(Reset)按钮,我要做的就是启用或禁用按钮.(button, all I want to do is either enable or disable the button.)
在绑定声明中,我添加了(In the binding declaration, I added the) Mode=OneWay
属性,因为更新仅从View Model到UI. UI永远不会更新该特定属性.因此,该属性在View Model类中被声明为只读.(property because the update only goes from the View Model to the UI. The UI never updates that particular property. And so the property is declared as read-only in the View Model class.)
并使用(And to use the) `` ,(, the)重启(Reset)按钮声明如下所示:(button declaration looks like this:)
<Button Content="Reset Game"
Style="{StaticResource EnableGameButtonStyle}"
Margin="430,190,0,0"
Click="btnReset_Click"/>
的(The) CheckBoxes
游戏网格下方也使用(below the game grid also uses the) IsEnableGameControls
属性与样式和触发器的语法相似.同样,我添加了另一个名为(property and the style and trigger syntax is silimar. Similarly, I added another property called) IsShowGameGrid
控制是否显示游戏网格.(that controls whether the game grid is displayed or not.)
要启用或禁用游戏控件,我只需调用(To enable or disable the game controls, I just call the) IsEnableGameControls
物业(property in the) ViewModel
:(:)
IsEnableGameControls = true;
并且绑定到该属性的所有控件都将受到影响.为了在WinForms中实现相同的功能,我必须编写类似以下内容的代码.我也必须用(And all controls that are bound to that property will be affected. To implement the same thing in WinForms, I would have to code something like the following. I would also have to wrap it with an) InvokeRequired
确保调用是线程安全的.(to make sure the call is threadsafe.)
private void EnableGameButtons(bool bEnable)
{
this.EnterNotesCheckbox.IsEnabled = bEnable;
this.ShowNotesCheckbox.IsEnabled = bEnable;
this.ShowSolutionCheckbox.IsEnabled = bEnable;
this.ResetButton.IsEnabled = bEnable;
}
由于我只是在改变(Since I am just changing the) IsEnabled
受影响的控件的属性以及(property of the affected controls and both the) IsEnabled
和(and) IsEnableGameControls
是布尔属性,我可以绑定(are boolean properties, I can just bind the) IsEnabled
每个控件的属性直接(property of each control directly to the) IsEnableGameControls
的属性(property of the) ViewModel
它将与使用(and it will work the same way as using the) Style Triggers
.该声明将如下所示:(. The declaration would look like this:)
IsEnabled="{Binding Path=IsEnableGameControls, Mode=OneWay}"
的(The) `` 声明(declaration for the)开始(Start)由于存在多个状态,因此按钮更为复杂.但是语法相似.(button is more complicated since there are multiple states. But the syntax is similar.)
<Style x:Key="StartButtonStyle"
TargetType="Button"
BasedOn="{StaticResource ButtonBaseStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=StartButtonState, Mode=OneWay}" Value="Start">
<Setter Property="Content" Value="Start Game"/>
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=StartButtonState, Mode=OneWay}" Value="Pause">
<Setter Property="Content" Value="Pause Game"/>
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=StartButtonState, Mode=OneWay}" Value="Resume">
<Setter Property="Content" Value="Resume Game"/>
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=StartButtonState, Mode=OneWay}" Value="Disable">
<Setter Property="Content" Value="Start Game"/>
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
每个触发器都绑定到(Each trigger is bound to the) StartButtonState
视图模型类中的属性.自从(property in the View Model class. Since the) StartButtonState
属性具有比简单布尔值更多的状态,我们为每个状态都包含一个触发器.在每个触发器标签中,我们设置要为每个状态更改的属性.我不必包括以下内容…(property has more states than a simple boolean, we include a trigger for each state. And within each trigger tag, we set the properties that we want to change for each state. I did not have to include the following…)
<Setter Property="IsEnabled" Value="True"/>
…在前三个触发标记中,因为此属性的默认值已经(…in the first three trigger tags since the default value of this property is already) True
而且我什么都没改变.但是我添加它们是为了提高可读性.(and I am not changing anything. But I added them for readability.)
同样,我们可以在(Likewise, we can create properties in the) ViewModel
返回内容(that return a content) string
以及每个的启用状态(and the enabled state for each) StartButtonState
并绑定(and bind the) Content
和(and the) IsEnabled
的属性(properties of the)开始(Start)这些属性的按钮.然后,我们可以绕过样式触发器的使用.(button to those properties. We can then bypass the use of Style Triggers.)
这是修改后的(Here is the modified) StartButtonState
物业(property in the) ViewModel
以及(as well as the) StartButtonContent
和(and the) IsEnableStartButton
特性:(properties:)
private StartButtonStateEnum StartButtonState
{
get
{
return _startButtonState;
}
set
{
_startButtonState = value;
OnPropertyChanged("StartButtonContent");
OnPropertyChanged("IsEnableStartButton");
}
}
public string StartButtonContent
{
get
{
switch (StartButtonState)
{
case StartButtonStateEnum.Pause:
return "Pause Game";
case StartButtonStateEnum.Resume:
return "Resume Game";
default:
return "Start Game";
}
}
}
public bool IsEnableStartButton
{
get
{
return (StartButtonState != StartButtonStateEnum.Disable);
}
}
并且"开始按钮" XAML代码如下所示:(And the Start Button XAML code looks like this:)
<Button Style="{StaticResource ButtonBaseStyle}"
Content="{Binding Path=StartButtonContent, Mode=OneWay}"
IsEnabled="{Binding Path=IsEnableStartButton, Mode=OneWay}"
Margin="430,105,0,0"
Click="btnStart_Click"/>
我们改变(We change the) `` 指向(to point to the) ButtonBaseStyle
并添加(and add the) Content
和(and the) IsEnabled
属性并将其绑定到(properties and bind them to the corresponding properties in the) ViewModel
类.(class.)
这里最大的是(The big thing here is in the) set
的访问者(accessor of the) StartButtonState
财产,我们称(property, we call the) OnPropertyChanged
两种方法(method on both the) StartButtonContent
和(and the) IsEnableStartButton
属性.(properties.)
就个人而言,这两种方法都行得通,我认为一种方法不比另一种更好.但是,使用样式触发器时的代码更少.但是,这会将某些业务逻辑放在UI中,而不是在(Personally, both methods work and I do not think that one is better than the other. There is less code when using Style Triggers though. However, that puts some of the business logic in the UI rather than in the) ViewModel
.因此,如果我们严格遵守MVVM编程模式,则第二种方法可能是首选的方法.这样,UI层不包含任何业务逻辑.(. So, if we were to strictly adhere to the MVVM programming pattern, then the second method would probably be the preferred method to use. That way, the UI layer does not contain any business logic.)
WPF和组合框(WPF and the Combo Box)
下一项业务是如何填充(The next order of business is how to populate the) ComboBox
与一个的内容(with the contents of an) Enum
.如果在Google中进行搜索,则有很多示例.让我们用微软自己的例子(. If one searches in Google, there are many examples on how to do this. Let us use this example from Microsoft’s own) 网站(website) .首先,我们需要声明名称空间(. First, we need to declare the namespace where the) enum
居住.在这个项目中,(resides. In this project, the) Enum
驻留在以下名称空间中:(resides in the following namespace:)
xmlns:srcME="clr-namespace:SudokuWPF.Model.Enums"
然后,在" Windows资源"部分中,声明以下内容:(Then, in the Windows Resource section, we declare the following:)
<ObjectDataProvider x:Key="GameLevels"
ObjectType="{x:Type sys:Enum}"
MethodName="GetValues">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="srcME:DifficultyLevels" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
这个XAML对象实际上是围绕(This XAML object is actually creating a wrapper around the) GetValues
的方法(method of the) Enum
类.而在(class. And in the) ComboBox
声明,我们指出(declaration, we point the) ItemsSource
该对象的属性,如下所示:(property to this object like so:)
<ComboBox HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="140"
ItemsSource="{Binding Source={StaticResource GameLevels}}"
SelectedItem="{Binding GameLevel}"
IsEditable="False"
IsReadOnly="True"
Canvas.Left="430"
Canvas.Top="32"/>
很简单吧?好吧,不完全是.我们之一(Pretty straightforward, right? Well, not exactly. One of our) enum
s是"(s is “) VeryEasy
“,这就是它在下拉列表中而不是”(” and that is exactly how it will appear in the dropdown instead of “) Very Easy
“,两个单词之间用空格隔开.要解决这个问题,网上有许多示例,介绍了如何解决此问题以及如何创建工具提示以及如何本地化(”, with a space between the two words. To get around this, there are many examples on-line on how to fix this as well as how to create tooltips and how to localize the) enum
价值观.但这对于该项目来说有点太多,所以让我们继续使用此方法,因为它易于阅读,而且丢失的空间并不明显.(values. But that is a bit much for this project so let us just stick to this method since it is easy to read and the missing space is not really noticeable.)
的(The) SelectedItem
属性绑定到中的另一个属性(property is bound to another property in the) ViewModel
类.在这种情况下,我们使用默认(class. In this case, we use the default) TwoWay
模式,因为我们希望更改同时进行.意思是,如果用户更改了选择,我们希望视图模型意识到这一点并采取行动.当游戏首次加载时,我们希望能够将级别设置为最后选择的级别.(mode since we want the changes to go both ways. Meaning, if the user changes the selection, we want the View Model to be aware of it and act on it. And when the game first loads, we want to be able to set the level to the last selected level.)
public DifficultyLevels GameLevel
{
get
{
return _gameLevel;
}
set
{
bool bLoadNewGame = (_gameLevel != value);
_gameLevel = value;
Properties.Settings.Default.Level = _gameLevel.GetHashCode();
if (bLoadNewGame)
LoadNewGame();
OnPropertyChanged();
}
}
因为我们绑定了(Because we bound the) SelectedItem
属性,我们不需要添加(property, we do not need to add the) SelectionChanged
组合框的属性以及后面代码中的相应事件处理程序.相反,通常(property to the combo box and a corresponding event handler in the code behind. Instead, the usual) SelectionChanged
事件代码位于(event code is found in the) set
此属性的访问者.(accessor of this property.)
在查看代码时,您会注意到一件事,即声明了所有绑定的属性(One thing you will notice when you look at the code, all the bound properties are declared) public
而不是我偏爱的访问修饰符,(instead of my preferred access modifier,) internal
.这是因为WPF需要该级别的访问权限.如果已声明(. This is because WPF requires that level of access. If it was declared) internal
,则XAML数据绑定将失败.对于此属性,即使(, then the XAML data binding will fail. For this property, even the) DifficultyLevels enum
必须声明(has to be declared) public
.(.)
游戏单元(Game Cells)
现在我们已经或多或少地处理了外围控件,我们如何管理游戏单元的状态?我们是否使用样式触发器?还是我们可以使用功能更强大的WPF工具?事实证明,有. WPF有一个(Now that we have more or less dealt with the periphery controls, how do we manage the state of the game cells? Do we use the Style triggers? Or is there a more powerful WPF tool that we can use? As it turns out, there is. WPF has a) DataTemplate
类.之所以使用它而不是简单的样式触发器,是因为每个游戏单元都可以显示两种主要数据.表示答案或用户答案的单个值.另一个状态是显示注释.但是,我们仍可能使用样式触发器来实现相同的效果.(class. The reason we use this instead of the simpler Style Triggers is because each game cell can display two primary kinds of data. A single value that denotes the answer or the user’s answer. And another state where it displays the notes. However, we could still probably use Style Triggers to achieve the same effects.)
首先,将每个游戏单元声明为(First, each game cell is declared as a) ContentControl
控制.一种(control. A) ContentControl
是WPF中几乎所有控件的基类.它代表具有任何类型的单个内容的控件.随着(is the base class for pretty much all the controls in WPF. It represents a control with a single piece of content of any type. With the) ContentControl
,我们可以构建自己的自定义控件来表示游戏单元.(, we can build our own custom control to represent the game cell.)
在里面(In the) Windows.Resources
部分,我们创建(section, we create the) DataTemplate
游戏单元.游戏单元的基础是3 x 3(for the game cell. The base for the game cell is a 3 x 3) Grid
这样我们就可以显示该单元格的注释.然后在每个网格中(so that we can display the notes for the cell. And in each grid, we put in a) TextBlock
元素以显示实际注释.如果我们显示的不是笔记,则使用另一个(element to display the actual note. If we are displaying anything other than notes, we use another) TextBlock
跨越整个网格区域.(that spans the entire grid area.)
突出显示单元格(Highlighting Cells)
网格的每个单元都有一个(Each cell of the grid has an) IsMouseDirectlyOver
我们可以用来添加样式触发器的属性,以在用户将鼠标悬停在某个单元格上方时将其突出显示.以下是我使用的样式触发器.我将其余样式排除在外,因为它并不是那么有趣.(property that we can use to add a style trigger to highlight it whenever the user mouses over a cell. The following is the style trigger I used. I excluded the rest of the style since it is not all that interesting.)
<Style.Triggers>
<Trigger Property="IsMouseDirectlyOver" Value="True">
<Setter Property="Background" Value="LightBlue" />
</Trigger>
</Style.Triggers>
使用此触发器,每当用户将鼠标移到游戏单元上方时,(With this trigger, whenever the user moves the mouse over a game cell, the) background
将更改为(will change to) LightBlue
.无论是否(. This works whether the) background
是(is) AliceBlue
要么(or) White
.(.)
WPF和数据模板(WPF and DataTemplates)
那么,我们如何处理不同的游戏单元状态?在原始的VB版本中,我使用了两个属性来控制单元状态:(So, how to do we deal with the different game cell states? In the original VB version, I used two properties to control the cell state:)
CellState
IsCorrect
虽然有这样的事情(While there is such a thing as a)MultiDataTrigger
,我认为合并这两个属性会更容易,并修改了(, I decided that it was easier to combine these two properties and modified the)CellStateEnum
相应地.另外,我需要更改Notes的工作方式.我添加了另一个类来表示每个单元格的注释状态,以便可以将每个注释单元格绑定到显示注释的属性.(accordingly. In addition, I needed to change the way the Notes worked. I added another class to represent the state of the Notes for each cell so that each note cell can be bound to a property that displays notes.) 查找(Look up the)CellDataTemplate
在里面(in the)MainWindow
XAML代码,看看我做了什么.(XAML code to see what I did.) 我还需要添加(I also needed to add the)INotifyPropertyChanged
连接到(interface to the)CellClass
和(and the)NoteState
由于这两个类都具有绑定到(since both classes have properties that are bound to controls in the)CellDataTemplate
.(.) 在里面(In the)NoteState
类,我有以下属性声明:(class, I have the following property declaration:)
public bool State
{
get
{
return _state;
}
set
{
_state = value;
OnPropertyChanged("Value");
}
}
该代码的作用是(What this code does is, in the) set
访问器,当注释的状态更改时,我们引发一个(accessor, when the state of the note is changed, we raise a) PropertyChanged
的事件(event for the) Value
属性.这是因为(property. This is because the) Value
属性绑定到(property is bound to a control on the) MainWindow
而不是(and not the) State
属性.这是提高(property. This is an example of raising the) PropertyChanged
相关属性上的事件.同样在(event on a related property. Also in the) ViewModel
类,我们具有以下功能:(class, we have the following function:)
private void UpdateAllCells()
{
if (IsValidGame())
foreach (CellClass item in _model.CellList)
OnPropertyChanged(item.CellName);
}
每当需要更新游戏网格中的所有单元格时,我们就调用此方法.这是另一个例子,我们提出了(We call this method whenever we need to update all the cells in the game grid. This is another example where we raise the) PropertyChanged
事件绑定到WPF控件的实际属性之外.(event outside of the actual property that is bound to a WPF control.)
输入板(Input Pad)
对于用户用于将值输入到空白单元格的输入板,WPF版本非常简单.这是在IDE设计器窗口中的外观:(For the Input Pad, which is used by the user to enter values into blank cells, the WPF version is pretty straight forward. This is how it looks in the IDE designer window:)
如前所述,我将(As I mentioned earlier, I moved the)暗示(Hint)和(and)明确(Clear)从主窗体上的按钮(buttons from the main form onto the)输入板(Input Pad)形成.基地(form. The base) Panel
我用于这种形式的是(I used for this form is the) StackPanel
.然后,我为数字键盘添加了一个3 x 3的网格,然后添加了两个按钮:一个用于(. I then added a 3 x 3 grid for the number pad and then two buttons: one for)暗示(Hint)而另一个(and another for)明确(Clear).(.)
要在这三个元素之间创建空格,我们添加(To create spaces between the three elements, we add the) Margin
属性的数字网格和底部(property to the number grid and the bottom)明确(Clear)按钮(button..)
当用户单击游戏网格上的某个单元时,我们希望将此窗口放置在用户刚刚单击的位置的旁边.为此,我们在单元格的click事件中使用以下代码行.(When the user clicks a cell on the game grid, we want to position this window right next to where the user just clicked. To do this, we use the following lines of code in the cell’s click event.)
System.Drawing.Point point = System.Windows.Forms.Control.MousePosition;
inputPad.Left = point.X + 20;
inputPad.Top = point.Y - (inputPad.Height / 2);
基本上,我们向系统查询鼠标的绝对位置.然后,我们调整(Basically, we query the system for the absolute location of the mouse. Then, we adjust the) Left
和(and) Top
“输入"窗口的"属性"并将其定位在鼠标单击右侧20像素处.创建和显示窗口的完整代码如下:(properties of the Input window and position it 20 pixels to the right of the mouse click. The complete code to create and display the window follows:)
inputPad = new InputPad();
inputPad.Owner = this;
System.Drawing.Point point = System.Windows.Forms.Control.MousePosition;
inputPad.Left = point.X + 20;
inputPad.Top = point.Y - (inputPad.Height / 2);
inputPad.ShowDialog();
与WinForms等效项一样,我们需要在Window上设置几个属性:(Like the WinForms equivalent, we need set several properties on the Window:)
ResizeMode="NoResize"
ShowInTaskbar="False"
SizeToContent="Height"
WindowStartupLocation="Manual"
WindowStyle="ToolWindow"
为了将此窗口放置在所需的位置(例如WinForms版本),我们需要设置(In order to position this window where we want it, like the WinForms version, we need to set the) WindowStartupLocation
财产(property to) Manual
.然后,我们可以控制(. Then, we can control the) Top
和(and) Left
窗口的属性.否则,Windows将忽略我们尝试重新定位窗口并将其放置在所需位置的任何尝试.(properties of the Window. Otherwise, Windows will ignore our attempts to reposition the window and just put it wherever it wants to.)
该窗口后面的代码也非常简单.用户单击某项后,我们将单击的按钮保存为状态,然后关闭窗口.(The code behind for this window is pretty straightforward as well. Once the user clicks something, we save the button clicked as a state and then close the window.)
的(The)关于(About)和(and)游戏完成(Game Complete)窗口是类似的,除了我们不需要保存任何状态.我们只是将子窗口作为模式对话框打开,然后在用户单击”(windows are similar except that we do not need to save any state. We just open the child window as a modal dialog and then when the user clicks “)好(OK)",只需将其关闭并返回主窗口即可.(”, just close it and return to the main window.)
包装WPF(Wrapping Up WPF)
一开始,我将代码中的所有click事件留给了(At the beginning, I left all the click events in the code behind for the) MainWindow
空白.但是现在(blank. But now that the) ViewModelClass
差不多完成了,下一步就是将它们连接在一起.基本上,(is more or less complete, the next step is to wire them up together. Basically, the) click
事件中(event in the) MainWindow
只是在(just calls a function in the) ViewModelClass
,类似于WinForms版本的做法.因此,"(, similar to how the WinForms version did. So, the code-behind for “)新游戏(New Game)“按钮如下所示:(” button looks like this:)
private void btnNew_Click(object sender, RoutedEventArgs e)
{
if (ViewModel != null)
ViewModel.NewClicked();
}
为了包装WPF版本,我清理了游戏代码(To wrap up the WPF version, I cleaned up the game play code in the) ViewModelClass
然后设置应用程序启动代码.的(and then set the Application startup code. The) Startup
WPF应用程序中的对象是(object in a WPF application is the)**App.XAML(App.XAML)**目的.当您打开它时,有一个(object. When you open it, there is a) StartupUri
物业(property in the) <Application/>
标签.默认情况下,它指向(tag. By default, it points to the) MainWindow.XAML.
但是由于我们要实例化(But since we want to instantiate the) ViewModelClass
外面的(outside of the) View
,我们将其指向(, we repoint it to the) ApplicationStartup
的方法(method of the) Application
类代替.另外,我们从(class instead. In addition, we rename it from) StartupURI
至(to) Startup
.然后,我们打开后台代码,并将以下方法添加到(. Then we open the code-behind and add the following method to the code-behind of the)**应用程式(App.xaml)**类.(class.)
public void ApplicationStartup(object sender, StartupEventArgs args)
{
MainWindow mainWindow = new MainWindow();
mainWindow.ViewModel = ViewModelClass.GetInstance(mainWindow);
mainWindow.Show();
}
因此,基本上,我们实例化主窗口,设置(So basically, we instantiate the main window, set the) ViewModel
属性为主窗口,然后显示该窗口.(property for the main window, and then display the window.)
如果您看一下后面的代码(If you look at the code-behind for the) MainWindow
,与WinForms应用程序的后台代码相比,它非常稀疏.(, it is pretty sparse compared to the code-behind for a WinForms application.)
这是WPF应用程序运行时的外观:(Here is how the WPF application looks like when it is running:)
如果您使用过该游戏的VB版本,您会注意到在该版本中,我实现了镜像,其中丢失的单元在垂直轴上进行了镜像.在游戏的下一版本中,我将添加另一个尺寸,其中游戏生成器会随机选择镜面为垂直,水平或对角线.(If you have played with the VB version of this game, you will notice that in this version, I have implemented the mirror image where the missing cells are mirrored on the vertical axis. In the next version of the game, I will add another dimension where the game generator randomly picks the mirror to be either vertical, horizontal, or diagonal.)
WPF和Silverlight(WPF and Silverlight)
现在,我们已经完成了从VB.NET/WinForms到C#/WPF的移植,现在我们将尝试将代码移植到Silverlight.我创建了一个新的C#Silverlight应用程序项目,添加了所有子文件夹((Now that we have completed the port from VB.NET/WinForms to C#/WPF, we shall now attempt to port the code to Silverlight. I created a new C# Silverlight Application project, added all the subfolders ()视图(View),(,)**模型(Model)**和(, and)视图模型(ViewModel)),然后将除WPF UI文件以外的所有文件从一个项目复制到另一个项目.(), then copied all the files, except for the WPF UI files, from one project to the other.) 我以为这将是一个非常直的港口,但是一路上有几个颠簸.第一个区别是Silverlight应用程序有两个项目,而不是一个.这些是:(I thought it would be a pretty straight port, but there were several bumps along the way. The first difference is that a Silverlight Application has two projects instead of one. These are:)
SilverlightApplication1
SilverlightApplication1.Web
第二个项目只是主项目的包装,因此我们可以在浏览器中运行它以进行测试/调试.因此,我们根本不必担心第二个项目.(The second project is just a wrapper for the main project so that we can run it in a browser for testing/debugging. So we do not have to concern ourselves with the second project at all.) 下一个区别是(The next difference is that instead of a)<Windows/>
标记在(tag in the)MainWindow
, 我们有一个(, we have a)<UserControl/>
在一个(in a)MainPage
.以下各节详细介绍了我尝试将代码从WPF移植到Silverlight时遇到的其他问题.(. The following sections detail the other bumps that I encountered as I tried to port the code from WPF to Silverlight.)
应用程序设置(Application Settings)
第一个难题是应用程序设置未移植到Silverlight.在WPF中,应用程序设置位于(The first bump was the application settings does not port over to Silverlight. In WPF, the application settings are found in a tab in the)应用属性(Application Property)窗口.(window.)
通过代码寻址属性类似于VB.NET Windows应用程序.(Addressing the properties from code is similar to VB.NET Windows application.)
Properties.Settings.Default.Level = (int)GameLevel;
Properties.Settings.Default.Save();
但是在Silverlight中,(But in Silverlight, the) Properties
名称空间不存在.但是,有(namespace does not exist. There is however, the) IsolatedStorageSettings
类.要使用它,需要像这样使用之前获取类的实例:(class. To use it, one needs to get an instance of the class before using it like so:)
private IsolatedStorageSettings _appSettings = IsolatedStorageSettings.ApplicationSettings;
在使用任何设置之前,我们首先检查该设置是否已经存在:(Before using any of the settings, we first check if the setting already exists or not:)
if (!_appSettings.Contains("Level"))
_appSettings.Add("Level", "");
因此,如果它不存在,我们将其添加.然后使用以下代码检索数据:(So, if it does not exist, we add it. Then to retrieve data, we use the following code:)
_level = (Int32)_appSettings["Level"];
要保存设置,我们使用以下代码:(To save the settings, we use the following code:)
_appSettings["Level"] = _level.ToString();
_appSettings.Save();
由于所有设置都保存为(Since all the settings are saved as a) string
,我们需要将整数值与(, we need to convert the integer value to and from a) string
当我们使用它.另外,要将数据提交到磁盘,我们需要调用(when we use it. Also, to commit the data to disk, we need to call the) Save()
方法.(method.)
由于需要一个此类的实例以及大量支持代码,因此我认为最好将所有设置合并为一个类,并将其包装为一个类.(Since one needs an instance of this class, as well as a lot of support code, I thought it best to consolidate all the settings into a single class and wrap it as a) Singleton
目的.(object.)
计时器类(Timer Class)
下一个障碍是(The next bump was the) Timer
类.显然,(class. Apparently,) System.Timers
不是Silverlight应用程序名称空间的一部分.幸运的是,还有另一个(is not part of the Silverlight application namespace. Fortunately, there is another) Timer
在里面(in the) System.Threading
命名空间.这两个类之间有一些区别,我不得不重写部分内容.(namespace. There are several differences between the two classes and I had to rewrite parts of the) Timer
类以适应这些差异.(class to accommodate those differences.)
第一个区别是如何实例化(The first difference is how one instantiates the) Timer
类.下一个区别是,没有(class. The next difference is, there is no) Enabled
属性以在计时器实例化后启动或停止计时器.但是,有一个(property to start or stop the timer once it is instantiated. There is, however, a) Change
一种可用于暂停/重新启动/停止计时器的方法.另外,一旦实例化并运行了计时器,就无法知道它是否正在运行,因此我创建了自己的计时器(method that one can use to pause/restart/stop the timer. Also, once the timer is instantiated and running, there is no way to know if it is running or not, so I created my own) TimerEnabled
属性以跟踪计时器的状态.(property to keep track of the state of the timer.)
UI XAML代码(The UI XAML Code)
解决上述问题后,我将WPF XAML代码从一个项目复制到了另一个项目.我没有复制实际文件,而是打开了原始WPF代码,将XAML代码复制到了(After fixing the above issues, I copied the WPF XAML code from one project to the other. Instead of copying the actual files, I opened the original WPF code, copied the XAML code inside the) <Window></Window>
的标签(tag for the) MainWindow
并粘贴到(and pasted it to the) MainPage.XAML
窗口.我也为其他三个子窗口执行了此操作.(window. I did this for the other three child windows as well.)
当我将代码粘贴到(When I pasted the code into the) MainPage.XAML
窗口中,有几条弯曲的线表示存在错误.错误包括:(window, there are several squiggly lines indicating that there are errors. Among the errors, are:)
DataTriggers
StatusBar
Label
ObjectDataProvider
由于我使用了(These are pretty serious issues since I used the)DataTrigger
类,以帮助控制和管理游戏控件和游戏网格.因此,我不得不寻找解决方法.(class to help control and manage the game controls and the game grid. So I had to look for work-arounds.)
状态栏和标签(The StatusBar And Labels)
这很容易解决.由于Silverlight没有(This was an easy enough fix. Since Silverlight did not have a) StatusBar
和(and the) Label
控件,我只是用了(controls, I just used the) <Textblock/>
控制.问题是(control instead. The issue was that the) Labels
被放在里面(were put inside the) StatusBar
控制.(control.)
由于Silverlight项目是(Since Silverlight projects are) UserControl
而不是窗口,我将背景面板对象从(as opposed to a Window, I switched the background panel object from a) Canvas
到一个(to a) Grid
.部分原因是因为(. Part of the reason was since this) UserControl
在浏览器窗口中运行且没有任何已定义的边框,因此将所有控件放置在网格中比较容易.因此,我添加了一些行和列,然后将每个控件放入特定的网格列和行中,并以这种方式对齐所有控件,而不是使用绝对坐标. UI在设计器窗口中的外观如下.(runs inside a browser window and does not have any defined border, it was easier to position all the controls a grid. So I added some rows and columns and then put each control into a specific grid column and row and aligned all the controls that way rather than using absolute coordinates. Here is how the UI looks in the designer window.)
这是另一个视图,其中显示了实际的网格行和列.(Here is another view with the actual grid rows and columns showing.)
如您所见,我创建了一个四列十行的网格.然后,我将每个控件分配给特定的行和列,并以这种方式排列它们.游戏网格本身跨越三列八行.然后,我将其中的游戏网格居中.状态栏(As you can see, I created a grid with four columns and ten rows. I then assigned each control to a particular row and column and lined them up that way. The game grid itself spans three columns and eight rows. I then centered the game grid within that. The status bar) TextBlock
在第十行,跨前三列.我在底部添加了一个灰色边框对象来模拟状态栏.(is in row ten and spans the first three columns. I added a gray border object to simulate a status bar at the bottom.)
对于具有多个控件的单元格,我使用了(For cells that have more than one control, I used a) StackPanel
对象作为基础,然后在其中对齐控件(object as the base and then aligned the controls within that) StackPanel
.例如,这是(. For example, here is the code for the) ComboBox
及其上方的标签:(and the label above it:)
<StackPanel Grid.Column="3"
Grid.Row="0">
<TextBlock Text="Difficulty Level:"
Height="20"
Margin="0,12,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom" />
<ComboBox Name="GameLevel"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemsSource="{Binding}"
SelectedIndex="0"
Width="140"
SelectionChanged="DifficultyLevel_SelectionChanged"/>
</StackPanel>
我定义一个(I define a) StackPanel
对象并将其分配给第三列和零行.然后我定义一个(object and assign it to column three and row zero. I then define a) TextBlock
对象,然后(object and then the) ComboBox
里面的物体(object inside that) StackPanel
.(.)
在右下角(In the lower right corner of the) MainPage
,我还用了(, I also used a) StackPanel
默认情况下元素在水平方向而不是垂直方向.这是一个示例,其中我定义了一种本地样式(where the elements are oriented horizontally as opposed to vertically, which is the default. Here is an example where I define a style that is local to that) StackPanel
目的.(object.)
<StackPanel.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="12" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="5" />
</Style>
</StackPanel.Resources>
既然我拥有(Since all I have in that) StackPanel
是(are) TextBlock
对象,我不需要为样式命名,因为我想要这个(objects, I do not bother with naming the style since I want this) 适用于所有(*to apply to all the*) `TextBlock` 里面的物体(*objects within that*) `StackPanel` .换句话说,这个范围(*. In other words, the scope of this*)
仅限于(is limited to the) StackPanel
定义它的对象.(object where it was defined.)
组合框ItemsSource(The ComboBox ItemsSource)
如果您注意到XAML(If you notice the XAML for the) ComboBox
以上,(above, the) ItemsSource
属性绑定到的东西不同于WPF版本.这是因为(property is bound to something different than the WPF version. This is because the) ObjectDataProvider
Silverlight中没有提供此类.解决方法是,我必须创建一个(class is not available in Silverlight. As a work-around, I had to create an) ObservableCollection
的(of) string
中的代码,然后将其绑定到(s in the code-behind and then bind it to the) ItemsSource
的属性(property of the) ComboBox
.这是代码:(. Here is the code:)
private ObservableCollection<string> _levels;
private void InitComboBox()
{
_levels = new ObservableCollection<string>();
foreach (string item in Enum.GetNames(typeof(DifficultyLevels)))
_levels.Add(item);
this.GameLevel.DataContext = _levels;
}
首先,我创建一个模块级变量,然后在(First, I create a module level variable, then in the) InitComboBox
方法,从中调用(method, which is called from the) MainPage_Loaded
事件中,我使用来自(event, I populate the collection with the names from the) Enum
.最后,由于(. Finally, since the) DataContext
此组合框与(for this combo box is different from the) MainPage
,我将(, I bind the) DataContext
将组合框直接移到集合中.这是我需要给(of the combo box directly to the collection. This is one of the cases where I needed to give the) ComboBox
控制名称.这样,我可以从后面的代码访问该控件.(control a name. That way, I could access the control from the code-behind.)
由于我绑定了(Since I bind the) DataContext
组合框的直接(of the combo box directly to the) ObservableCollection
,(, the) ItemsSource
绑定定义如下所示:(binding definition looks like this:)
ItemsSource="{Binding}"
我们不必在绑定声明中指定路径或属性.(We do not have to specify a path or property in the binding declaration.)
我也可以创建一个(I could have also created an) ObservableCollection
物业(property in the) ViewModel
类并绑定组合框的(class and bound the combo box’s) ItemsSource
属性,而不是后面的代码中.(property to that property rather than in the code behind.)
您可能会注意到的另一个区别是(Another difference that you might notice is that the) ComboBox
的(’s) SelectedIndex
属性未绑定到中的属性(property is not bound to a property in the) ViewModel
课,我也定义了一个(class and that I also defined a) SelectionChanged
点击事件.这是有原因的,稍后我将继续解释WPF和Silverlight之间的区别时,将进行解释.(click event. There is a reason for this and I will explain later on when I continue with the differences between WPF and Silverlight.)
Silverlight和DataTriggers(Silverlight and DataTriggers)
我对Silverlight没有它感到震惊(I was kind of shocked that Silverlight does not have) DataTrigger
s.那么我将如何使用为WPF项目编写的所有精美触发器?好吧,Silverlight有一个叫做(s. So how was I going to use all the fancy triggers that I wrote for the WPF project? Well, Silverlight has something called) DataTemplates
.为了在用户将鼠标悬停在单元格上时突出显示它们,我使用了(. For highlighting the cells as the user mouses over them, I used the) VisualStateManager
类.(class.)
我基本上必须重做模型并创建一个(I had to essentially redo the model and created an) abstract
BaseCell
类.然后,我添加了(class. I then added an) AnswerCell
,(,) BlankCell
,(,) HintCell
等等都是从(, etc. that all inherit from the) BaseCell
每个州的班级(class for each state in the) CellStateEnum
.自从(. Since the) BaseCell
完成后,派生单元不需要具有任何其他属性或方法.(is complete, the derived cells do not need to have any extra properties or methods.)
这里是(Here is the) BaseCell abstract
有一个单一的类(class which has a single) public
属性基于(property based on the) CellClass
:(:)
public abstract class BaseCell
{
public CellClass Cell { get; set; }
}
然后其他每个状态类都从此派生(Then each of the other state classes derive from this) abstract
类.它们看起来都一样,所以我将仅包括(class. They all look the same so I will just include the definition for the) AnswerCell
类.(class.)
public class AnswerCell : BaseCell
{ }
如您所见,每个类都继承自(As you can see, each class inherits from the) abstract
类,并且类定义为空白.(class and the class definition is blank.)
然后,对于每个状态类,我创建了一个数据模板.所以,对于(Then, for each of these state classes, I created a data template. So, for the) Answer
类,对应(class, the corresponding) DataTemplate
看起来像这样.但是首先,我需要创建基本单元格样式.(looks like this. But first, I needed to create the base cell style.)
<Style x:Key="CellOutputStyle"
TargetType="TextBlock">
<Setter Property="Height" Value="39"/>
<Setter Property="Width" Value="34" />
<Setter Property="TextAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="FontSize" Value="24" />
</Style>
然后,(Then, the) DataTemplate
为了(for the) AnswerCell
看起来像这样:(looks like this:)
<DataTemplate DataType="model:AnswerCell">
<Grid>
<TextBlock Style="{StaticResource CellOutputStyle}"
Text="{Binding Path=Cell.Answer}"/>
</Grid>
</DataTemplate>
的(The) Text
的财产(property of) TextBlock
绑定到基础细胞的(is bound to underlying cell’s) Answer
属性.(property.)
然后,我创建一个(I then create a) DataTemplate
每个单元状态数据类型.基本上,每个(for each and every cell state data type. Basically, each) DataTemplate
从基地开始(starts out with a base) Grid
在网格内,我添加了一个(and inside the grid, I add a) TextBlock
控制以显示数据.我还设置了(control to display data. I also set the) Foreground
颜色属性取决于显示的是答案(黑色,默认值),提示(紫色)还是用户的正确(绿色)或错误的答案(红色).(color property to depending on whether it is displaying the answer (black, default), a hint (purple), or the user’s correct (green) or incorrect answer (red).)
我还需要添加(I also need to add the) Model.Structures
XAML代码顶部的名称空间,以便它知道这些类所在的位置.(namespace to the top of the XAML code so that it knows where these classes reside.)
xmlns:model="clr-namespace:Sudoku_Silverlight.Model.Structures"
然后(Then the) Content
游戏网格中每个单元的属性都绑定到(property of each cell in the game grid is bound to a) Cell
物业(property in the) ViewModel
返回(that returns the) BaseCell
类.因此,在此示例中,(class. So, in this example, the) Content
第一个单元格(行零,列零)的属性绑定到(property of the first cell (row zero, column zero) is bound to the) Cell00
的属性(property of the) ViewModel
类.(class.)
<Button Style="{StaticResource CellStyleBlue}"
Content="{Binding Path=Cell00, Mode=OneWay}"
Grid.Column="0"
Grid.Row="0"
Click="A0_CellClick" />
和的定义(And the definition for) Cell00
在里面(in the) ViewModel
类看起来像这样:(class looks like this:)
public BaseCell Cell00
{
get
{
return GetCell(0, 0);
}
}
然后,我在模型和(I then add an extra data layer between the model and the) ViewModel
基本上是(which is basically a list of) BaseCell
对象.(objects.)
private List<BaseCell> Cells { get; set; }
然后,当我需要显示不同的状态时,我只需将列表中特定索引处的单元格对象替换为相应的类即可.例如,如果我想在第一个单元格中显示答案,则只需将列表中的第一个元素分配给(Then when I need to display the different states, I simply replace the cell object at a particular index in the list with the corresponding class. For example, if I want to display the answer in the first cell, I simply assign the first element in the list to an) AnswerCell
像这样的课.(class like so.)
Cells[0] = new AnswerCell() { Cell = item };
哪里(where) item
是实际的(is the actual) CellClass
来自的对象(object from the) Model
对于第一个单元格.然后我举起(for the first cell. Then I raise the) PropertyChanged
该单元格的事件,因此UI会使用(event for that cell so the UI will update the cell contents with the) DataTemplate
为了(for the) AnswerCell
.(.)
OnPropertyChanged(item.CellName);
如果您注意到,在Cell的XAML代码中,没有指向(If you notice, in the XAML code for the Cell, there is no pointer to the) DataTemplate
我们之前创建的但是因为单元格绑定到抽象(that we created earlier. But because the cell is bound to the abstract) BaseCell
依次指向(and that in turns points to an) AnswerCell
类,Silverlight使用(class, Silverlight uses the) AnswerCell DataTemplate
格式化该单元格.很酷吧?(to format that cell. Pretty cool, right?)
如果我正确执行此操作,则我将修改(If I were to do this properly, I would have modified the) Model
层以输出此列表,而不是在(layer to output this list instead of creating it in the) ViewModel
层.另外,我将使用索引器,这样就不必为每个单元格定义属性.但这有效,所以让我们保持现状.(layer. Also, I would use indexers so that I do not have to define a property for each cell. But this works so let us leave it the way it is.)
在Silverlight中突出显示单元(Highlighting Cells in Silverlight)
我需要翻译的最终触发器是在用户将鼠标悬停在每个单元格上方时突出显示该单元格.我们通过使用(The final trigger I needed to translate was highlighting the cell as the user mouses over each cell. We do this by using the) VisualStateManager
类.这是执行此操作的XAML代码:(class. Here is the XAML code to do this:)
<Style x:Key="CellStyleBlue"
TargetType="ContentControl"
BasedOn="{StaticResource BaseCellStyle}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Grid HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="3">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<!--Take one half second
to transition to the MouseOver state.-->
<VisualTransition To="MouseOver"
GeneratedDuration="0:0:0.5"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal" />
<!--Change the SolidColorBrush (ButtonBrush)
to CadetBlue when the
mouse is over the button.-->
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Storyboard.TargetName="ButtonBrush"
Storyboard.TargetProperty="Color"
To="CadetBlue" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.Background>
<SolidColorBrush x:Name="ButtonBrush" Color="AliceBlue" />
</Grid.Background>
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
这看起来很复杂,但是实际上很简单.我们创建一个(This looks pretty complicated, but it is actually pretty simple. We create a) 在那(*and within that*)
,我们添加一个(, we add a) Template
属性定义我们关注的视觉状态.基本上,我们关心(property to define the visual states that we care about. Basically, we care about the) Normal
和(and) MouseOver
状态.当用户将鼠标悬停在某个单元格上时,我们会更改(states. When the user mouses over a cell, we change the) Background
的属性(property of the) Grid
来自的对象(object from) AliceBlue
至(to) CadetBlue
.此外,请花费0.5秒进行过渡.(. In addition, take 0.5 seconds to make the transition.)
即使我们在(Even though we are not doing anything in the) Normal
状态,如果我们删除该行,即使鼠标离开该单元格,该单元格也会保持突出显示状态.(state, if we remove that line, the cell will remain highlighted even after the mouse leaves the cell.)
有趣的是,即使所有样式都以(The interesting thing here is that even though the Styles all target the) ContentControl
,我实际上将样式应用于(, I actually apply the style to a) Button
控制.当我将其更改为(control. When I change it to a) ContentControl
,突出显示不起作用,但其他所有功能都起作用.我不确定为什么突出显示无法正常工作.也许在写完这篇文章之后,我将花一些时间来尝试解决它.(, the highlighting does not work, but everything else does. I am not sure why the highlighting does not work properly. Maybe after writing this article, I will spend some time to try and figure it out.)
WPF应用程序中的突出显示与Silverlight应用程序之间的区别之一是,无论基础数据如何,突出显示都会针对Silverlight版本中的所有单元格出现.在WPF版本中,如果基础数据是"答案”,则突出显示被禁用.这是因为在Silverlight中,(One of the differences between the highlighting in the WPF application versus the Silverlight application is that the highlight appears for all the cells in the Silverlight version regardless of the underlying data. In the WPF version, if the underlying data is the Answer, the highlighting is disabled. This is because in Silverlight, the) IsEnabled
缺少属性(property is missing for the) TextBlock
控制.(control.)
无效的跨线程访问错误(Invalid Cross-thread Access Error)
进行必要的更改以编译Silverlight项目之后,我尝试运行它.吐出”(After making the necessary changes to get the Silverlight project compiled, I tried to run it. It did not get far before it spit an “)无效的跨线程访问错误(Invalid Cross-thread access error)在其中一个(” in one of the) OnPropertyChanged
事件.显然,Silverlight不像WPF那样处理多线程应用程序.我搜索了Google,解决方案是使用(events. Apparently, Silverlight does not handle multi-threaded applications as well as WPF does. I searched Google and the solution was to use the) Dispatcher.CheckAccess()
检查跨线程访问的方法.使用方法如下:(method to check for cross-thread access. Here is how it is used:)
private void SetTextBlockContent(TextBlock target, string value)
{
if (target.Dispatcher.CheckAccess())
target.Text = value;
else
target.Dispatcher.BeginInvoke(() => { target.Text = value; });
}
这与WinForms应用程序存在相同的问题,在WinForms应用程序中,不允许后台线程直接访问UI控件.相反,需要检查(This is the same problem with WinForms applications where background threads are not allowed to access the UI controls directly. Instead, one needs to check) InvokeRequired
.这是WinForms的等效项:(. Here is the WinForms equivalent:)
delegate void SetTextCallback(string text);
private void SetText(string text)
{
if (this.textBox1.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
this.textBox1.Text = text;
}
我本可以花更多时间尝试找出哪些人需要(I could have spent more time trying to figure out which ones need the) Dispatcher
哪些没有.相反,我只是决定使用(and which ones do not. Instead, I just decided to use the) Dispatcher
几乎每件事.由于使用(pretty much every thing. Since using the) Dispatcher
就像使用(is just like using) InvokeRequired
在WinForms中,我决定使用自定义事件来获取(in WinForms, I decided to use custom events to get the) ViewModel
与(to talk to the) View
反之亦然.虽然我仍然提出(and vice versa. While I still raise the) PropertyChanged
一些事情的事件,我仍然设置(event for some things and I still set the) DataContext
的属性(property of the) MainPage
到(to the) ViewModel
,我启动了一个自定义事件,用于在两者之间进行几乎任何形式的交流,以使事情正常进行.(, I fire off a custom event for pretty much any kind of communication between the two just to get things working properly.)
之前我提到组合框(Earlier, I mentioned that the combo box) SelectedIndex
属性未指向ViewModel中的属性.而且我还为(property does not point to a property in the ViewModel. And that I also defined an event handler for the) SelectionChanged
事件.我这样做的原因是由于此Dispatcher问题.因此,当用户在组合框中更改选择时,它将触发具有新选择和(event. The reason I did this was because of this Dispatcher issue. So, when the user changes the selection in the combo box, it fires an event with the new selection and the) ViewModel
全班都在听变化.(class is listening for the change.)
进行所有这些更改之后,Silverlight项目的代码隐藏看起来就像WPF和WinForms之间的交叉.(After making all these changes, the code-behind of the Silverlight project looks like a cross between WPF and WinForms.)
无效的XML(Invalid XML)
当我输入XAML代码时,出现了另一个问题(Another issue came up when I was entering XAML code for the) MainPage
. XAML编辑器窗口将在我输入的新代码下显示一条波浪线.当我将鼠标悬停在波浪线上方时,编辑器会告诉我底层代码是"(. The XAML editor window would display a squiggly line under the new code I was entering. When I mouse over the squiggly line, the editor would tell me that the underlying code was “)无效的XML(Invalid XML)".它会抱怨以下内容:(.” It would complain for something as simple as the following:)
<DataTemplate DataType="model:AnswerCell">
</DataTemplate>
搜索完Google之后,答案与我输入的XAML代码无关,而是应用程序的名称.创建项目时,我将其命名为"(After searching Google, the answer had nothing to do with the XAML code I was entering, but rather, it was the name of the application. When I created the project, I named it “) Sudoku Silverlight
“之间有一个空格.显然,这会在整个地方引起问题,这是症状之一.进入项目属性页面并删除该空格后,"(” with a space in between. Apparently, this causes problems all over the place and this is one of the symptoms. After going to the project properties page and removing the space, the “)无效的XML(Invalid XML)错误消失了.那真是一个奇怪的错误.(” error went away. That was a really weird error.)
因此,最重要的是,命名项目时要小心,不要在名称中使用空格.(So bottom line, be careful when naming your project and do not use spaces in the name.)
子视窗(Child Windows)
因为Silverlight在浏览器中运行,所以打开子窗口通常与WPF或Windows应用程序不同.一方面,它无权访问鼠标位置.其次,它无法控制窗口的放置位置. Silverlight始终将其置于浏览器窗口的中心. Silverlight缺少的另一件事是(Because Silverlight works inside a browser, opening a child window is different from a WPF or Windows application in general. For one thing, it does not have access to the mouse position. Second, it does not have any control over where the window is placed. Silverlight will always center it within the browser window. Another thing that is missing in Silverlight is the) ShowDialog
方法.它只有(method. It only has) Show
.以来(. Since) Show
是非模态的,它将在打开子窗口后返回,而不会等待子窗口关闭.(is non-modal, it will return after opening the child window and does not wait around for the child window to close.)
考虑到这些差异,我不得不以不同于WinForms或WPF应用程序的方式来处理子窗口,尤其是Input Pad窗口.幸运的是,有一个(With those differences in mind, I had to tackle the child windows, especially the Input Pad window, differently than I would in a WinForms or WPF application. Fortunately, there is a) Closed
可以连接事件处理程序的事件.(event that one can wire up an event handler to.)
这是打开"输入面板"窗口的代码:(Here is the code that opens the Input Pad window:)
private void UIShowNumberPad(CellIndex callingCell)
{
InputPad inputPad = new InputPad(callingCell);
inputPad.Closed += NumberPadClosed;
inputPad.Show();
}
我创建子窗口的实例,连接事件处理程序,然后将其打开.这是处理(I create an instance of the child window, wire up the event handler, then open it. Here is the corresponding code that handles the) Closed
事件.(event.)
private void NumberPadClosed(object sender, EventArgs e)
{
InputPad inputPad = (InputPad)sender;
if (inputPad.InputPadState != InputPadStateEnum.InvalidState)
RaiseEvent(inputPad.CallingCell, inputPad.InputPadState);
}
当用户通过单击”(When the user closes the Input Pad window by either clicking the “) X
“或窗口上的按钮,我引发了一个包含一些数据的事件,以便(” or a button on the window, I raise an event with some data so that the) ViewModel
知道最后一次单击哪个单元格以及用户单击了输入板上的哪个按钮.(knows which cell was last clicked as well as which button on the Input Pad the user clicked.)
Silverlight中的消息框(Message Box In Silverlight)
不像一个(Unlike a) MessageBox
在WinForms应用程序中(in a WinForms application, the) MessageBox
在Silverlight中有几个限制,我决定不使用它,因为我无法控制按钮中的文本.默认情况下,它具有一个”(in Silverlight has several limitations, I decided not to use it because I could not control the text in the buttons. By default, it has an “)好(OK)按钮.方法调用中有一个参数,我也可以在其中添加一个(” button. There is a parameter in the method call where I can also add a “)取消(Cancel)“按钮.但是我不能将按钮文本更改为”(” button as well. But I cannot change the button text to say “)是(Yes)“和”(” and “)没有(No)",因此我决定创建自己的消息框.(”, so I decided to create my own Message Box.)
并不是那么困难.我用了(It was not all that difficult. I used a) Grid
作为基础,并添加了三列和两行.对于第二行(按钮所在的位置),(as the base and added three columns and two rows. For the second row, where the buttons are located, the) Height
设置为"(was set to “) Auto
“.(”.)
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
与其将高度在两行之间平均分配,(Instead of splitting the height evenly between the two rows,) Auto
将行高设置为该行上控件的高度.其余的都提供给第一行.关于此窗口的工作方式,它类似于"输入面板"窗口的工作方式.我把一个(sets the row height to whatever the height is of the controls on that row. And the rest of it is given to the first row. As for how this window works, it is similar to the way the Input Pad window works. I wire up a) Closed
事件处理程序,它返回用户是否单击的信息(event handler and it returns information whether the user clicked)是(Yes)要么(or)没有(No).(.)
结束Silverlight项目(Wrapping Up The Silverlight Project)
最后,代码的背后(In the end, the code-behind for the) MainPage
看起来像WPF和WinForms之间的交叉.发表本文后,我可能会花一些时间看看是否可以减少(looks like a cross between WPF and WinForms. After publishing this article, I might spend some time to see if I can reduce the number of) Dispatcher
呼叫以及在两次呼叫之间引发的自定义事件的数量(calls as well as the number of custom events that are raised between the) View
和(and the) ViewModel
.(.)
解决了WPF和Silverlight之间的差异并清理了游戏代码之后,最后一步是包装应用程序开始/停止代码.由于该项目在浏览器中运行,因此我们无法从内部关闭该应用程序.因此,不需要关闭按钮.但是,我们确实知道应用程序何时关闭,因为(After working through the differences between WPF and Silverlight and cleaning up the game play code, the final step was to wrap up the application start/stop code. Since the project runs in a browser, we cannot close the application from within. So there is no need for a close button. We do, however, know when the application closes because there is an) Exit
事件中(event in the) Application
类.(class.)
我打开(I open the)**App.xaml.cs(App.xaml.cs)**归档并连接(file and wire up the) Application_Exit
处理程序,当用户关闭运行该应用程序的选项卡或浏览器时,它将调用一种方法来停止所有后台线程,并将当前设置保存到(handler in such a way that when the user closes the tab or browser where this application is running, it will call a method to stop all the background threads as well as save the current settings to the) IsolatedStorageSettings
.(.)
这是Silverlight应用程序在运行时的外观:(Here is how the Silverlight application looks like when running:)
它看起来非常接近WPF版本.主要区别在于右下角的数字之间没有标题栏和行分隔符.我还添加了一个(It looks pretty close to the WPF version. The main difference is the lack of a title bar and line separators between the numbers in the lower right corner. I also added a) border
用户控件周围的元素,以便当它在浏览器中运行时,用户知道游戏窗口的边界在哪里.(element around the user control so that when it runs in a browser, the user knows where the bounds of the game window is.)
结论(Conclusion)
这是一个有趣的项目,试图将VB.NET WinForms应用程序转换为C#/WPF和Silverlight.我对WPF和Silverlight都学到了很多.尽管Silverlight是WPF的子集,但两者之间仍然存在足够的差异,因此共享XAML代码将是一项艰巨的任务.对我来说,最大的不同是Silverlight中缺少触发器.(This was a fun project to try to convert a VB.NET WinForms application to C#/WPF and Silverlight. I learned a lot about both about WPF and Silverlight. Even though Silverlight is a subset of WPF, there are enough differences between the two that sharing XAML code would be a difficult task. The biggest difference for me is the lack of triggers in Silverlight.) 但是,如果一个项目确实需要在两者之间共享相同的代码库,并且经过周密的计划和大量的编译器指令,则可以完成该工作.在线上有几篇文章讨论了WPF,Silverlight和Windows Phone/Store App之间的代码重用和代码共享.基本上,采用最低公分母并编写代码以使用该子集.如果操作正确,代码将可在所有三个平台上移植.(However, if a project really needed to share the same code base between the two, with careful planning and lots of compiler directives, it can be done. There are several articles available on-line that talk about code re-use and code sharing between WPF, Silverlight, and Windows Phone/Store App. Basically, take the lowest common denominator and write code to use that subset. If done right, the code will be portable across all three platforms.) 对于这个项目,如果我是从Silverlight开始的,那么将代码移植到WPF可能比其他方法容易得多.也许以后再解决这个问题.也就是说,尝试将Silverlight项目移植到WPF,并查看实际上有多少可重用.(For this project, had I started out with Silverlight, it might have been much easier to port the code to WPF than the other way around. Maybe I will tackle that question at a later date. That is, try to port the Silverlight project to WPF and see just how much is actually re-usable.) 我希望通过详细介绍我经历的所有问题,对其他人的项目有所帮助.(I hope that by detailing all the issues I went through, it will help someone else out with their project.) 您会注意到的一件事是,我为游戏网格中的每个单元创建了一个属性以及一个匹配的click事件处理程序.得出81个属性和81个单击事件.我确信可以通过将WPF XAML中的参数与单元格的行和列一起传递来简化此过程.我将要研究的另一件事.(One of the things that you will notice is that I created a property for each cell in the game grid as well as a matching click event handler. That comes out to 81 properties and 81 click events. I am sure there is a way to simplify that by passing parameters from the WPF XAML with the cell’s row and column. It will be another thing that I will look into.) 如有任何关于本文中任何问题的疑问或问题,或者如果您希望我进一步了解我上面提到或未能解决的特定问题或主题,请随时与我联系.(Feel free to contact me with questions or issues about anything in this article or if you would like me to go into more details about a particular issue or topic that I covered above or failed to cover.)
历史(History)
- 2015年1月1日:代码/文章的第一版(2015-01-01: First release of the code/article)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
HTML C# .NET Windows WPF Silverlight VS2013 ASP.NET Dev Architect 新闻 翻译