图形用户界面应用程序较之控制台界面应用程序最大的好处就是界面友好、数据显示直观。CUI程序中数据只能以文本的形式线性显示,GUI程序则允许数据以文本、列表、图形等多种形式立体显示。
用户体验在GUI程序设计中起着举足轻重的作用-----用户界面设计成什么样看上去才足够的漂亮?控件如何安排才简单易用并且少犯错误?这些都是设计师需要考虑的问题。WPF系统不但支持传统的Winfrom编程的用户界面和用户体验设计,更支持使用专门的设计工具Blend进行专业设计,同时还推出了以模板为核心的新一代设计理念。
1.1 模板的内涵
从字面上看,模板就是“具有一定规格的样板”,有了它,就可以依照它制造很多一样是实例。我们常把看起来一样的东西称为“一个模子里面刻出来的。”就是这个道理。然而,WPF中的模板的内涵远比这个深刻。
Binding和基于Binding数据驱动UI是WPF的核心部分,WPF最精彩的部分是什么呢?依我看,既不是美轮美奂的3D图形,也不是炫目多彩的动画,而是默默无闻的模板(Template)。实际上,就连2D/3D绘图也常常是为它锦上添花。
Templdate究竟有什么能力能够使得它在WPF体系中获此殊荣呢?这还要从哲学谈起,“形而上者谓之道,形而下者谓之器”,这句话出自《易经》,大意是我们能够观察到的世间万物形象之上的抽象的结果就是思维,而形象之下掩盖的就是其本质。显然,古人已经注意到“形”是连接本质和思维的枢纽,让我们把这句话引入计算机世界。
软件之道并非本书研究的主要类容,本书研究的是WPF。WPF全称Windows Presentation Foundation,Presentation一词的意思就是外观,呈现,表现,也就是说,在WIndows GUI程序这个尺度上,WPF扮演的就是“形”的角色、是程序的外在形式,而程序的内容仍然是由数据和算法构成的业务逻辑。与WPF类似,Winform和Asp.net也都是内容的表现形式。
让我们把尺度缩小到WPF内部。这个系统与程序内容(业务逻辑)的边界是Binding,Binding把数据源源不断从程序内部送出来交由界面元素来显示,又把从界面元素搜集到的数据传回程序内部。界面元素间的沟通则依靠路由事件来完成。有时候路由事件和附加事件也会参与到数据的传输中。让我们思考一个问题:WPF作为Windows的表示方式,它究竟表示的是什么?换句话说,WPF作为一种“形式”,它表现的内容到底是什么?答案是程序的数据和算法----Binding传递的是数据,事件参数携带的也是数据;方法和委托的调用是算法,事件传递消息也是算法----数据在内存里就是一串串字符或字符。算法是一组组看不见摸不着的抽象逻辑,如何恰如其分的把它们展现给用户呢?
加入想表达一个bool类型,同时还想表达用户可以在这两个值之间自由切换这样一个算法,你会怎么做?你一定会想使用一个CheckBox控件来满足要求;再比如颜色值实际上是一串数字,用户基本上不可能只看数字就能想象出真正的颜色,而且用户也不希望只靠输入字符来表示颜色值,这时,颜色值这一“数据内容”的恰当表现形式就是一个填充着真实颜色的色块。,而用户即可以输入值又可以用取色吸管取色来设置值的“算法内容”恰当的表达方式是创建一个ColorPicker控件。相信你已经发现,控件(Control)是数据内容表现形式的双重载体。换句话说,控件即是数据的表现形式让用户可以直观的看到数据,又是算法的表现形式让用户方便的操作逻辑。
作为表现形式,每个控件都是为了实现某种用户操作算法和直观显示某种数据而生,一个控件看上去是什么样子由它的“算法内容”和“数据内容决定”,这就是内容决定形式,这里,我们引入两个概念:
我想,尽管你还没有学习什么DataTempldate,但借助前面学习的基础一样可以看个八九不离十了。
public class Car
{
public string AutoMark { get; set; }
public string Name { get; set; }
public string Year { get; set; }
public string TopSpeed { get; set; }
}
///
/// CarListViewItem.xaml 的交互逻辑
///
public partial class CarListViewItem : UserControl
{
public CarListViewItem()
{
InitializeComponent();
}
private Car car;
public Car Car
{
get { return car; }
set
{
car = value;
this.txtBlockName.Text = car.Name;
this.txtBlockYear.Text = car.Year;
this.igLogo.Source = new BitmapImage(new Uri(@"Resource/Image/"+car.AutoMark+".png",UriKind.Relative));
}
}
}
类似的原理,我们需要为Car类型准备一个详细信息视图。UserControl名称为CarDetailView,XAML部分代码如下:
后台支持数据大同小异:
///
/// CarDetailView.xaml 的交互逻辑
///
public partial class CarDetailView : UserControl
{
public CarDetailView()
{
InitializeComponent();
}
private Car car;
public Car Car
{
get { return car; }
set
{
car = value;
this.txtBlockName.Text = car.Name;
this.txtBlockAutoMark.Text = car.AutoMark;
this.txtBlockYear.Text = car.Year;
this.txtTopSpeed.Text = car.TopSpeed;
this.imgPhoto.Source = new BitmapImage(new Uri(@"Resource/Image/" + car.Name + ".jpg", UriKind.Relative));
}
}
}
最后把它们组装到窗体上:
窗体的后台代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace WpfApplication1
{
///
/// Window35.xaml 的交互逻辑
///
public partial class Window35 : Window
{
public Window35()
{
InitializeComponent();
InitialCarList();
}
private void listBoxCars_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
CarListViewItem viewItem = e.AddedItems[0] as CarListViewItem;
if(viewItem!=null)
{
carDetailView1.Car = viewItem.Car;
}
}
private void InitialCarList()
{
List infos = new List() {
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="200", Year="1990"},
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="250", Year="1998"},
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="300", Year="2002"},
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="350", Year="2011"},
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="500", Year="2020"}
};
foreach (Car item in infos)
{
CarListViewItem viewItem = new CarListViewItem();
viewItem.Car = item;
this.listBoxCars.Items.Add(viewItem);
}
}
}
public class Car
{
public string AutoMark { get; set; }
public string Name { get; set; }
public string Year { get; set; }
public string TopSpeed { get; set; }
}
}
//厂商名称转换为Logo路径
public class AutoMarkToLogoPathConverter:IValueConverter
{
///
/// 正向转
///
///
///
///
///
///
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new BitmapImage(new Uri(string.Format(@"Resource/Image/{0}.png",(string)value),UriKind.Relative));
}
///
/// 逆向转未用到
///
///
///
///
///
///
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
//汽车名称转换为照片路径
public class NameToPhotoPathConverter:IValueConverter
{
///
/// 正向转
///
///
///
///
///
///
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new BitmapImage(new Uri(string.Format(@"Resource/Image/{0}.jpg", (string)value), UriKind.Relative));
}
///
/// 逆向转未用到
///
///
///
///
///
///
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
代码对于初学者来说有点长但是结构非常简单。其中最重要的有两句:
///
/// Window36.xaml 的交互逻辑
///
public partial class Window36 : Window
{
public Window36()
{
InitializeComponent();
InitialCarList();
}
private void InitialCarList()
{
List infos = new List() {
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="200", Year="1990"},
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="250", Year="1998"},
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="300", Year="2002"},
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="350", Year="2011"},
new Car(){ AutoMark="Aodi", Name="Aodi", TopSpeed="500", Year="2020"}
};
this.lbInfos.ItemsSource = infos;
}
}
运行程序,效果如下图:
这段代码有以下几个看点:
//
// button1
//
this.button1.Location = new System.Drawing.Point(1100, 199);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 23);
this.button1.TabIndex = 0;
this.button1.Text = "报表";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// printPreviewDialog1
//
this.printPreviewDialog1.AutoScrollMargin = new System.Drawing.Size(0, 0);
this.printPreviewDialog1.AutoScrollMinSize = new System.Drawing.Size(0, 0);
this.printPreviewDialog1.ClientSize = new System.Drawing.Size(400, 300);
this.printPreviewDialog1.Enabled = true;
this.printPreviewDialog1.Icon = ((System.Drawing.Icon)(resources.GetObject("printPreviewDialog1.Icon")));
this.printPreviewDialog1.Name = "printPreviewDialog1";
this.printPreviewDialog1.Visible = false;
同样的逻辑如果在XAML代码里出就变成了这样:
程序运行效果如下图:
Darren
Andy
Jacky
T-Soft
Darren
Andy
Jacky
T-Soft
public class Unit
{
public string Year{get;set;}
public int Price{get;set;}
}
程序XAML代码如下:
private void StackPanel_Click(object sender, RoutedEventArgs e)
{
MenuItem item = e.OriginalSource as MenuItem;
XmlElement xe = item.Header as XmlElement;
MessageBox.Show(xe.Attributes["Name"].Value);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
TextBox tb = this.uc.Template.FindName("textbox1", this.uc) as TextBox;
tb.Text = "Hello WPF";
StackPanel sp = tb.Parent as StackPanel;
(sp.Children[1] as TextBox).Text = "Hello ControlTemplate";
(sp.Children[2] as TextBox).Text = "I Can Find YOU.";
}
public class Student38
{
public int Id { get; set; }
public string Name { get; set; }
public string Skill { get; set; }
public bool HasJob { get; set; }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
TextBlock tb = this.cp.ContentTemplate.FindName("txtBlockName", this.cp) as TextBlock;
MessageBox.Show(tb.Text);
//Student38 stu = this.cp.Content as Student38;
//MessageBox.Show(stu.Name);
}
public class Student39
{
public int Id { get; set; }
public string Name { get; set; }
public string Skill { get; set; }
public bool HasJob { get; set; }
}
程序运行效果如下图:
因为我们是在DataTemplate里面添加了事件处理器,所以界面上任何一个由此DataTemplate生成的TextBox都会在获得焦点的时候调用txtBoxName_GotFocus这个事件处理器。txtBoxName_GotFocus的代码如下:
private void txtBoxName_GotFocus(object sender, RoutedEventArgs e)
{
TextBox tb = e.OriginalSource as TextBox; //获取事件发起的源头
ContentPresenter cp = tb.TemplatedParent as ContentPresenter;//获取模板目标
Student39 stu = cp.Content as Student39;//获取业务逻辑数据
this.lvStudent.SelectedItem = stu;//设置ListView选中项
//访问界面元素
ListViewItem lvi = this.lvStudent.ItemContainerGenerator.ContainerFromItem(stu) as ListViewItem;
CheckBox cb = this.FindVisualChild(lvi);
MessageBox.Show(cb.Name);
}
private ChildType FindVisualChild(DependencyObject obj) where ChildType : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj,i);
if (child != null && child is ChildType)
{
return child as ChildType;
}
else
{
ChildType childOfChild = FindVisualChild(child);
if(childOfChild!=null)
{
return childOfChild;
}
}
}
return null;
}
}
运行效果如下图:
public class L2BConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
int textLength = (int)value;
return textLength > 6 ? true : false;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
后台代码如下:
public Window43()
{
InitializeComponent();
InitialInfo();
}
private void InitialInfo()
{
List infos = new List() {
new Student38(){ Id=2, Name="Darren", Skill="WPF"},
new Student38(){ Id=1, Name="Tom", Skill="Java"},
new Student38(){ Id=3, Name="Jacky", Skill="Asp.net"},
new Student38(){ Id=2, Name="Andy", Skill="C#"},
};
this.lbInfos.ItemsSource = infos;
}