用 Windows Forms 和 WPF 创建相同的程序






4.84/5 (48投票s)
展示如何在 Windows Forms 和 WPF 中编写同一个简单的程序。
引言
本文展示了同一个简单程序的两种实现。首先,我们检查 Windows Forms 版本,然后是 WPF 版本。本文的目的是向 WinForms 程序员展示创建 WPF 应用程序的简单示例。在此过程中,我们将比较和对比使用这两个平台的工作。
本文没有对 WPF 或 WinForms 大加赞扬,只是讨论了如何使用它们进行开发的异同。我撰写本文并非旨在说服人们转向 WPF,或继续使用 Windows Forms。它只是回顾了我在每个平台上创建相同程序时的经验,并展示了如何在 WPF 中创建一个简单的程序。
我使用 Visual Studio 2008 用 C# 编写了这两个应用程序。
背景
许多 WinForms 开发人员通过在 Visual Studio 的 WinForms 设计界面上拖放控件来创建用户界面。我也是这样创建演示 WinForms 应用程序的。为了在两个平台之间的开发进行有意义的比较,我还使用了 Visual Studio 2008(又名 Cider)中的 WPF 设计界面来开发 WPF 版本。我没有清理 Cider 生成的 XAML。我确实不得不稍微编辑一下 XAML,我们稍后会讨论。
我必须承认,截至 Visual Studio 2008,我并不是很喜欢使用 Cider。我通常是手动编写 XAML,而不是让 Cider 替我生成。除了我的 XAML 比 Cider 的 XAML 更清晰、格式更好之外,我发现手动编写 XAML 更容易理解 UI 的结构。你可能会对需要手动编写所有这些 XML 的前景感到惊讶,但我可以向你保证,一旦你达到一定的熟练程度,这是一种非常高效的操作模式。
微软承认大多数人都认为我偏爱手动编写 XAML 是疯子,并且正在努力改进 WPF 的设计时体验。对于我们这些习惯了像 WinForms 这样成熟技术所提供的出色设计时支持的人来说,Cider 目前还有很多不足之处。我们稍后将探讨一些差异。
本文没有深入探讨 WPF,也没有深入解释所使用的平台功能。如果您是 WPF 的新手并想了解更多信息,可以查看我的五部分系列文章《WPF 引导之旅》。另外,请务必阅读Sacha Barber关于 WPF 的介绍性文章,其中第一篇在这里。
程序概述
本文重点介绍的演示应用程序非常简单。除了显示虚构公司的一些员工之外,它没有做太多事情。用户可以编辑每个员工的姓名,但没有对空值进行输入验证检查。我试图让 WinForms 和 WPF 版本看起来非常相似。我还试图通过尽我所能地利用每个平台来公平对待两者。
这是 WinForms 版本的屏幕截图
这是 WPF 版本的屏幕截图
如果您编辑员工的名字或姓氏,并将输入焦点移动到另一个字段,则该员工的全名将更新以反映更改。这表明应用程序的每个版本中的控件都绑定到 Employee
对象的属性。
共享业务对象
这两个应用程序都使用来自同一个 BusinessObjects 类库项目的同一个 Employee
类。该类对于 WinForms 应用程序和 WPF 应用程序都是相同的。没有基于 UI 平台包含/排除任何代码的预处理器块,或者类似的东西。这是类定义(请记住,这是 C# 3.0 代码)
public class Employee : INotifyPropertyChanged
{
#region Creation
public static Employee[] GetEmployees()
{
// In a real app this would probably call into a
// data access layer to get records from a database.
return new Employee[]
{
new Employee(1, "Joe", "Smith",
GetPictureFile(1), new DateTime(2000, 2, 12)),
new Employee(2, "Frank", "Green",
GetPictureFile(2), new DateTime(2002, 7, 1)),
new Employee(3, "Martha", "Piccardo",
GetPictureFile(3), new DateTime(2003, 1, 20)),
};
}
private static string GetPictureFile(int employeeID)
{
string fileName = String.Format("emp{0}.jpg", employeeID);
string folder = Path.GetDirectoryName(
Assembly.GetExecutingAssembly().Location);
folder = Path.Combine(folder, "Images");
return Path.Combine(folder, fileName);
}
private Employee(int id, string firstName, string lastName,
string pictureFile, DateTime startDate)
{
this.ID = id;
this.FirstName = firstName;
this.LastName = lastName;
this.PictureFile = pictureFile;
this.StartDate = startDate;
}
#endregion // Creation
#region Properties
public int ID { get; private set; }
string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
if (value == _firstName)
return;
_firstName = value;
this.OnPropertyChanged("FirstName");
this.OnPropertyChanged("FullName");
}
}
string _lastName;
public string LastName
{
get { return _lastName; }
set
{
if (value == _lastName)
return;
_lastName = value;
this.OnPropertyChanged("LastName");
this.OnPropertyChanged("FullName");
}
}
public string FullName
{
get { return String.Format("{0}, {1}",
this.LastName, this.FirstName); }
}
public string PictureFile { get; private set; }
public DateTime StartDate { get; private set; }
#endregion // Properties
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
这里没有什么不寻常的,只是一些普通的 C# 代码。请注意,Employee
实现了 INotifyPropertyChanged
接口,WinForms 和 WPF 都理解它。这将在稍后绑定 FullName
属性时发挥作用。
WinForms 版本
Windows Forms 程序有一个 Form
和一个名为 EmployeeControl
的自定义 UserControl
。Form
包含一个 FlowLayoutPanel
,其中包含每个 Employee
的一个 EmployeeControl
实例。这是我编写的 Form
代码
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
// Create and initialize a usercontrol for each employee.
foreach(Employee emp in Employee.GetEmployees())
{
EmployeeControl empCtrl = new EmployeeControl();
empCtrl.Employee = emp;
this.flowLayoutPanel.Controls.Add(empCtrl);
}
}
}
EmployeeControl
,它是一个 UserControl
,在 Visual Studio 设计界面上进行布局。由于每个 EmployeeControl
都显示一个 Employee
对象的属性值,我利用了出色的 WinForms 设计时支持来获取 Employee
对象的“模式”,然后在“属性”窗口中建立数据绑定。当我告诉 Visual Studio 我的 EmployeeControl
将绑定到 BusinessObjects 程序集中的 Employee
类实例时,它自动为我创建了一个配置了 Employee
元数据的 BindingSource
组件。这使得绑定(几乎)所有控件变得轻而易举。
如果您不熟悉这个非常有用的工具,它显示在下面的屏幕截图中,展示了 lastNameTextBox
的 Text
属性如何绑定到 employeeBindingSource
的 LastName
属性
我能够使用该工具创建所有数据绑定,除了 PictureBox
的工具提示和 Employee
的 ID
属性之间的绑定。我必须在代码中进行设置。我为 EmployeeControl
编写的代码如下所示
/// <summary>
/// A WinForms control that displays an Employee object.
/// </summary>
public partial class EmployeeControl : UserControl
{
public EmployeeControl()
{
InitializeComponent();
// Convert the picture file path to a Bitmap.
Binding binding = this.employeePicture.DataBindings[0];
binding.Format += this.ConvertFilePathToBitmap;
}
void ConvertFilePathToBitmap(object sender, ConvertEventArgs e)
{
e.Value = Bitmap.FromFile(e.Value as string);
}
public Employee Employee
{
get { return this.employeeBindingSource.DataSource as Employee; }
set
{
this.employeeBindingSource.DataSource = value;
// The employee's picture shows a tooltip of their ID.
if (value != null)
{
string msg = "Employee ID: " + value.ID;
this.toolTip.SetToolTip(this.employeePicture, msg);
}
}
}
}
上述代码有两点需要注意。我必须处理 PictureBox
的 Image
属性的 Binding
对象的 Format
事件。这是必要的,以便我可以将 Employee
对象的 PictureFile
属性的 string
转换为 Bitmap
实例。如果我不这样做,Image
属性绑定将失败,因为它无法将 string
分配给 Image
类型的属性。
另外,请注意,当设置 Employee
属性时,我给 PictureBox
一个工具提示消息。工具提示显示员工的 ID
,前缀为“Employee ID:”。我尝试在设计器中设置此绑定,但找不到方法。
总的来说,在 Windows Forms 中创建这个简单的应用程序非常容易。大部分工作都在可视化设计器中完成,其余部分只需要少量代码。Windows Forms 绝对是一个出色的快速应用程序开发 (RAD) 平台。
WPF 版本
我无需编写一行 C# 代码即可在 WPF 中创建此程序。工作包括在 Cider 中拖放,然后在 Cider 为我编写的 XAML 中进行一些简单的编辑。如果我有一位可视化设计师与我一起工作,我本可以向他/她展示 Employee
类的属性,并让他/她为我完成所有工作。从某种意义上说,我想这平衡了 Cider 当前的不足,因为我根本不需要使用它! :)
演示应用程序的 WPF 版本有一个 Window
和一个名为 EmployeeControl
的自定义 UserControl
。这与 WinForms 版本设置相同。WPF 应用程序不是使用 WinForms 的 FlowLayoutPanel
来承载代码中创建的 EmployeeControl
,而是使用 ItemsControl
来承载用户控件。此外,ItemsControl
负责为我创建 EmployeeControl
,所以我不需要在 Window
的代码隐藏中编写一个循环来创建它们。
这是 Window
的 XAML 文件(请记住,我没有清理 Cider 的 XAML,除了稍微格式化了一下)
<Window
x:Class="WpfApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
xmlns:model="clr-namespace:BusinessObjects;assembly=BusinessObjects"
Title="WPF App" Height="558" Width="503"
WindowStartupLocation="CenterScreen"
>
<Window.DataContext>
<ObjectDataProvider
ObjectType="{x:Type model:Employee}"
MethodName="GetEmployees"
/>
</Window.DataContext>
<Grid>
<Label
Name="label1"
HorizontalContentAlignment="Center" VerticalAlignment="Top"
FontSize="20" FontWeight="Bold"
Height="36.6" Margin="0,16,0,0"
>
Employees:
</Label>
<ItemsControl
ItemsSource="{Binding}"
HorizontalContentAlignment="Center"
Margin="46,59,50,0"
Focusable="False"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:EmployeeControl />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
关于这个 Window
的 XAML 有几点需要注意。它的 DataContext
被分配了一个 ObjectDataProvider
对象,该对象调用我的 Employee
类的静态 GetEmployees
方法。一旦 DataContext
设置为 Employee
对象的数组,将 ItemsControl
的 ItemsSource
设置为“{Binding}”意味着该控件将显示所有这些 Employee
。
我们不需要在代码隐藏中循环为每个 Employee
创建 EmployeeControl
实例。这是因为 ItemsControl
的 ItemTemplate
属性被设置为一个 DataTemplate
,它将为列表中的每个 Employee
创建一个 EmployeeControl
。由于 ItemsControl
的 ItemsSource
绑定到 Employee
对象数组,并且其 ItemTemplate
知道如何创建 EmployeeControl
,因此没有理由编写我们 WinForms 示例中看到的任何代码。
我必须手动编辑 XAML 文件才能创建 XML 命名空间别名、ObjectDataProvider
、ItemsControl
的 ItemTemplate
,甚至 ItemsControl
本身。我不确定为什么 Visual Studio 工具箱默认不包含 ItemsControl
。我还必须编辑 XAML 以包含所有绑定,因为 Cider 不允许您在设计界面上创建数据绑定。
EmployeeControl
的代码隐藏也是空的,除了创建新 UserControl
时自动编写的强制性 InitializeComponent
调用。与 WinForms 应用程序不同,我能够创建所有数据绑定而无需编写代码(但我确实必须在 XAML 中创建所有绑定)。
这是 EmployeeControl
的 XAML。请注意:我通常避免在我的文章中展示大量无趣、格式不佳的 XAML,但在此示例中我破例了,因为我想让您对 RAD 场景中 Cider 创建的 XAML有一个真实的印象
<UserControl x:Class="WpfApp.EmployeeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="137" Width="372">
<Border
BorderBrush="Black"
BorderThickness="1"
Margin="2"
SnapsToDevicePixels="True">
<Grid Height="129">
<Image Source="{Binding PictureFile}"
Margin="2" Name="image1" Stretch="None"
Width="96" Height="125" HorizontalAlignment="Left" >
<Image.ToolTip>
<TextBlock>
<Run TextBlock.FontWeight="Bold">Employee ID:</Run>
<TextBlock Margin="4,0,0,0" Text="{Binding ID}" />
</TextBlock>
</Image.ToolTip>
</Image>
<Label
Content="{Binding FullName}"
Height="34" Margin="99,2,0,0"
Name="fullNameLabel"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
FontSize="16" FontWeight="Bold" />
<Label Margin="108,34,0,0" Name="firstNameLabel"
FontWeight="Bold" Height="28"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Width="73">First Name:</Label>
<TextBox
Text="{Binding FirstName}"
HorizontalAlignment="Right" Margin="0,39,10,0"
Name="textBox1" Width="172" Height="23"
VerticalAlignment="Top" TextDecorations="None" />
<Label FontWeight="Bold" Height="28" Margin="108,0,0,34"
Name="lastNameLabel" VerticalAlignment="Bottom"
HorizontalAlignment="Left"
Width="73">Last Name:</Label>
<TextBox
Text="{Binding LastName}"
Height="23" Margin="0,0,10,34" Name="textBox2"
VerticalAlignment="Bottom" HorizontalAlignment="Right"
Width="172" />
<Label Height="28" Margin="108,0,185,2"
Name="startDateLabel" VerticalAlignment="Bottom"
FontWeight="Bold">Start Date:</Label>
<Label
Content="{Binding StartDate}"
Height="28" HorizontalAlignment="Right" Margin="0,0,10,2"
Name="startDateValueLabel" VerticalAlignment="Bottom"
Width="172" />
</Grid>
</Border>
</UserControl>
我需要编辑这个 XAML 文件来创建 Image
元素的 ToolTip
,并为各种绑定元素创建绑定。除此之外,所有 XAML 都是由 Cider 编写的,而我则将控件从工具箱拖放到设计界面并调整属性值。如果我手动编写这个 XAML,它可能会小一半。
请注意,Image
的工具提示包含与 WinForms PictureBox
的工具提示相同的消息,但在 WPF 中,可以对工具提示的文本应用格式。由于 WPF 中的 ToolTip
可以包含任何类型的内容,我可以在其中放置一个微型、轻量级的文档。这使得我的工具提示能够更接近 UI 的其余部分,其中标题文本使用粗体。
结论
作为一个编程平台,Windows Forms 要求我编写代码来完成 WPF 中使用标记即可轻松完成的事情。作为一个 RAD 环境,Windows Forms 允许我通过 Visual Studio 中直观的向导和属性网格完成 90% 的 UI 工作。WPF 要求我经常深入到 XAML 文件并进行调整,因为它还没有丰富的开发时支持。
对于在 WinForms 方面经验丰富的开发人员来说,被要求编辑标记可能看起来很奇怪和不自然。对于 ASP.NET 开发人员来说,这似乎是第二天性,因为他们已经习惯了使用标记文件和代码隐藏文件。归根结底,大多数 WPF 用户发现手动编辑 XAML 是一种乐趣!
本文并非旨在让您倾向或远离 WPF 或 WinForms。我们只是粗略地了解了这两个平台。如果您正在决定下一个项目是否转向 WPF,希望本文能帮助您做出更明智的决定。如果您一直想知道一个简单的 WPF 应用程序与等效的 WinForms 应用程序相比是什么样子,也许本文满足了您的好奇心。如果您正在寻找解释 WPF 为什么出色或糟糕的原因,也许我为您提供了一些弹药。:)
祝您编码愉快!