理解并实现装饰器模式

背景 本文讨论装饰器模式,这个模式是因为很多情况下需要动态的给对象添加功能.比如我们创建了一个Stream类.后来需要对这个数据流类动态的添加一个加密功能.有人可能说把加密方法写到流类里面啊.然后使用一个bool变量来控制开关就行了.但是这样.这个加密方法只能写一种..如果用派生类来实现.那么..对于不同的加密方法.,都要创建一个子类,举个例子.比如有时候是一些函数的组合.我们最终的派生类的数目基本上就和排列组合的数目一样了. 我们使用装饰器模式来解决这个问题.GoF描述为 “Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.” 首先看一下图.理解一下这个模式中每一个类的作用 • Component:定义了可以动态添加功能的具体类ConcreteComponents的接口. • ConcreteComponent: 可以动态添加功能的具体类 • Decorator: 定义了动态添加到ConcreteComponent类中的功能的接口 • ConcreteDecorator: 可以添加到 ConcreteComponent.中的具体功能类. 使用代码 我们开一个面包店的例子.面包店卖蛋糕和甜点.客户可以买蛋糕和甜点,同时添加一些额外的东西.额外的东西包括奶油(Cream),樱桃(Cherry),香料(Scent)和会员(Name Card) 如果我们用派生类来实现..那么我们会有如下的类 • CakeOnly • CakeWithCreamAndCherry • CakeWithCreamAndCherryAndScent • CakeWithCreamAndCherryAndScentAndNameCard • CakeWithCherryOnly • PastryOnly • PastryWithCreamAndCherry • PastryWithCreamAndCherryAndScent • PastryWithCreamAndCherryAndScentAndNameCard • PastryWithCherryOnly • 等等等等 这简直就是噩梦..我们用装饰器模式来实现把. 首先定义Component 接口 public abstract class BakeryComponent { public abstract string GetName(); public abstract double GetPrice(); } 前面说过了.这个类定义了能够动态添加功能的具体类(ConcreteComponents)的接口,好吧.然后来创建具体类ConcreteComponents class CakeBase : BakeryComponent { // 真实世界里,这个值应该来自数据库等 private string m_Name = "Cake Base"; private double m_Price = 200.0; public override string GetName() { return m_Name; } public override double GetPrice() { return m_Price; } } class PastryBase : BakeryComponent { //真实世界里,这个值应该来自数据库等 private string m_Name = "Pastry Base"; private double m_Price = 20.0; public override string GetName() { return m_Name; } public override double GetPrice() { return m_Price; } } 现在基对象准备好了.看看那些可以被动态添加的功能.我们看看Decorator 类 ...

2012-10-22 · 2 min · bystander

理解并实现原型模式-实现ICloneable接口.理解深浅拷贝

本文用C#实现原型模式,也会讨论深浅拷贝,已经如何在.net中高效实现ICloneable 接口 介绍 有时候我们需要从上下文得到一个对象的拷贝,然后通过一些独立的操作来处理他。原型模式在这种情况下很适用 GoF 定义原型模式为用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype." 看一下类图 主要的参与者有 • Prototype: 抽象类或接口,定义了方法来拷贝自己 • ConcretePrototype: 克隆的具体类. • Client: 需要执行拷贝对象的软件对象 然后实现吧 使用代码 为了简化。我以一个著名的偷车游戏作为例子 我们说游戏里有一个注脚。这个主要有着一些定义游戏数据的统计量。保存游戏的时候我们就需要拷贝这个对象,然后序列化到文件中。(仅仅是举个例子,真实的游戏里很少这样做) 下面这个类抽象类就是概念中的Prototype public abstract class AProtagonist { int m_health; int m_felony; double m_money; public int Health { get { return m_health; } set { m_health = value; } } public int Felony { get { return m_felony; } set { m_felony = value; } } public double Money { get { return m_money; } set { m_money = value; } } public abstract AProtagonist Clone(); } 接口定义了玩家重要的信息,然后定义了一个Clone方法。然后我们定义一个具体的玩家类CJ。这样我们可以克隆当前对象,然后异步的进行序列化 class CJ : AProtagonist { public override AProtagonist Clone() { return this.MemberwiseClone() as AProtagonist; } } 这个类就是概念中的ConcretePrototype 。这里为了简化也没有其他一些方法了。 现在看看客户端软件的写法 static void Main(string[] args) { // 演示原型模式 CJ player = new CJ(); player.Health = 1; player.Felony = 10; player.Money = 2.0; Console.WriteLine("Original Player stats:"); Console.WriteLine("Health: {0}, Felony: {1}, Money: {2}", player.Health.ToString(), player.Felony.ToString(), player.Money.ToString()); // 这里是拷贝部分. CJ playerToSave = player.Clone() as CJ; Console.WriteLine("\nCopy of player to save on disk:"); Console.WriteLine("Health: {0}, Felony: {1}, Money: {2}", playerToSave.Health.ToString(), playerToSave.Felony.ToString(), playerToSave.Money.ToString()); } ...

2012-10-19 · 3 min · bystander

实现IEnumerable接口&理解yield关键字

本文讨论题目的内容。然后讨论IEnumerable接口如何使得foreach语句可以使用。之后会展示如果实现自定义的集合类,该集合类实现了IEnumerable接口。Yield关键字和遍历集合后面也讨论。 背景 一使用集合。就发现遍历集合就跟着来了。遍历集合最好的方式是实现迭代器模式-Understanding and Implementing the Iterator Pattern in C# and C++(这篇文章我过几天翻译一下) ,C#提供foreach来以一种优雅的方式遍历 只要集合实现了IEnumerable 接口就可以用foreach来遍历。 使用代码 首先先看一下内置的集合类如何使用foreach来遍历的。ArrayList实现了IEnumerable 接口。我们看一下 // 看一下实现了IEnumerable 接口的集合如何遍历 ArrayList list = new ArrayList(); list.Add("1"); list.Add(2); list.Add("3"); list.Add('4'); foreach (object s in list) { Console.WriteLine(s); } 遍历泛型集合类 Arraylist 是一个通用集合类,遍历泛型集合类也可以。因为这些泛型集合类实现了IEnumerable<T>接口,看一下吧。 // 遍历实现了IEnumerable<T>接口的泛型类 List<string> listOfStrings = new List<string>(); listOfStrings.Add("one"); listOfStrings.Add("two"); listOfStrings.Add("three"); listOfStrings.Add("four"); foreach (string s in listOfStrings) { Console.WriteLine(s); } 发现了吧。我们自定义的集合类或是泛型集合类应该实现IEnumerable和IEnumerable<T>接口。这样就可以遍历了。 理解yield关键字 在写个实现接口的例子之前,先理解一下yield关键字,yield会记录集合位置。当从一个函数返回一个值的时候,yield可以用。 如下的普通的方法。不论调用多少次,都只会返回一个return static int SimpleReturn() { return 1; return 2; return 3; } static void Main(string[] args) { // 看看 Console.WriteLine(SimpleReturn()); Console.WriteLine(SimpleReturn()); Console.WriteLine(SimpleReturn()); Console.WriteLine(SimpleReturn()); } 原因就是普通的return语句不保留函数的返回状态。每一次都是新的调用。然后返回第一个值。 但是使用下面的语句替换后就不一样。当函数第二次调用的时候。会从上次返回的地方继续调用 static IEnumerable<int> YieldReturn() { yield return 1; yield return 2; yield return 3; } static void Main(string[] args) { // 看看yield return的效果 foreach (int i in YieldReturn()) { Console.WriteLine(i); } } 显然返回1,2,3,唯一要注意的就是函数需要返回IEnumerable。,然后通过foreach调用。 在自定义的集合类里实现Ienumerable接口 现在如果我们在我们的自定义集合里定义一个方法。来迭代所有元素。然后通过使用yield返回。我们就可以成功了。 好。我们定义MyArrayList 类,实现IEnumerable 接口,该接口就会强制我们实现GetEnumerator 函数。这里我们就要使用yield了。 class MyArrayList : IEnumerable { object[] m_Items = null; int freeIndex = 0; public MyArrayList() { // 对了方便我直接用数组了,其实应该用链表 m_Items = new object[100]; } public void Add(object item) { // 考虑添加元素的时候 m_Items[freeIndex] = item; freeIndex++; } // IEnumerable 函数 public IEnumerator GetEnumerator() { foreach (object o in m_Items) { // 检查是否到了末尾。数组的话。。。没写好 if(o == null) { break; } // 返回当前元素。然后前进一步 yield return o; } } } ...

2012-10-19 · 2 min · bystander

模拟Office2010文件菜单的TabControl模板

这是Office2010中的文件菜单点开后的效果。本文我将以强大的WPF来实现类似的效果。希望你能有所收获。而不是只拷贝/粘贴代码而已。 开始之前。先把TabControl找个地方放着。 <Window x:Class="TestClient.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <TabControl Name="tabSteps"> <TabItem Header="Info" IsSelected="True"> <TextBlock>Info content</TextBlock> </TabItem> <TabItem Header="Recent"> <TextBlock>Recent content tab</TextBlock> </TabItem> <TabItem Header="New"> <TextBlock>New content tab</TextBlock> </TabItem> <TabItem Header="Print"> <TextBlock>Print content tab</TextBlock> </TabItem> <TabItem Header="Save &amp; Send"> <TextBlock>Save &amp; send content tab</TextBlock> </TabItem> <TabItem Header="Help"> <TextBlock>Help tab</TextBlock> </TabItem> </TabControl> </Window> 然后会大概是这个效果 为了改变TabControl的显示效果。我们使用模板机制,我们把模板写进一个资源字典里。这样就可以重用了。添加一个资源字典的步骤如下 右键点击工程-添加-资源字典 然后在资源字典里添加一些代码。 <ControlTemplate x:Key="OfficeTabControl" TargetType="{x:Type TabControl}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="160" /> <ColumnDefinition/> </Grid.ColumnDefinitions> <Border Background="#FFE9ECEF" Grid.Column="0" BorderBrush="LightGray" BorderThickness="1" SnapsToDevicePixels="True" /> <StackPanel IsItemsHost="True" Grid.Column="0" Margin="0,0,-1,0" SnapsToDevicePixels="True" /> <ContentPresenter Content="{TemplateBinding SelectedContent}" Grid.Column="1" Margin="15,0,0,0" /> </Grid> </ControlTemplate> 这样就添加了一个有一个grid元素的名为OfficeTabControl的控件模板 . Grid 被分成两列,一列是标签页,一列是页内容。左边的列包含一个灰色背景和亮灰色的边缘线,然后一个StackPanel,IsItemsHost属性被设置为true, 这样标签项被会放在这个栈面板里。第二列是ContentPresenter 这会放置标签页内容。然后让我们前面的TabControl使用新模板。设置Template 属性。 <Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="OfficeTab.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources> <TabControl Name="tabSteps" Template="{StaticResource OfficeTabControl}"> 在这之前,先把资源字典加到窗体的Reesouce里。然后再设置。然后运行软件。效果会有一些不一样。 然后要修改左侧单个标签的显示效果。通过改变模板来实现。给模板添加如下的代码 <ControlTemplate x:Key="OfficeTabControl" TargetType="{x:Type TabControl}"> <ControlTemplate.Resources> <Style TargetType="{x:Type TabItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TabItem}"> <Grid SnapsToDevicePixels="True"> <ContentPresenter Name="buttonText" Margin="15,0,5,0" TextBlock.FontFamily="Calibri" TextBlock.FontSize="12pt" TextBlock.Foreground="Black" Content="{TemplateBinding Header}" VerticalAlignment="Center"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ControlTemplate.Resources> 然后再运行 VisualState很有意思。我们可以放在Grid里。然后设置正常状态和鼠标悬停的状态。 为了添加鼠标悬停效果,我们添加两个Borders元素。一个右边缘是灰线,另一个用在背景上。亮蓝色放在上下边缘 <Border Name="hoverShape" Height="40" Margin="0,0,1,0" SnapsToDevicePixels="True" BorderThickness="0,0,1,0" BorderBrush="LightGray"> <Border BorderBrush="#FFA1B7EA" BorderThickness="0,1" Background="#FFE5EEF9" Height="40" SnapsToDevicePixels="True" /> </Border> 之后,我们为VisualState创建故事板,一个是正常状态。会使得hoverShape的透明度为0.另一个是鼠标悬停的状态。透明度会变成1 <Grid SnapsToDevicePixels="True"> <VisualStateManager.VisualStateGroups> <VisualStateGroup Name="CommonStates"> <VisualState Name="MouseOver"> <Storyboard> <DoubleAnimation Storyboard.TargetName="hoverShape" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:.1"/> </Storyboard> </VisualState> <VisualState Name="Normal"> <Storyboard> <DoubleAnimation Storyboard.TargetName="hoverShape" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:.1"/> </Storyboard> </VisualState> </VisualStateGroup> 之后效果如下 ...

2012-10-17 · 2 min · bystander

类型安全的黑板模式(属性包)

有时候对于对象来说。在一个软件中,不直接通过互相引用而做到共享信息是非常有用的。比如像带有插件的软件。可以互相进行通信。假设我们有了很多对象。其中一些包含一些数据。而另一些对象需要消费这些数据 不同的子集,我们不通过对数据生产者和消费者的直接引用来实现,而是通过更低耦合的方式。叫做创建一个“BlackBoard”(黑板)对象。该对象允许其他对象自由对其进行读取/写入数据。这种解耦方式使得消费者不知道也不必知道数据来自哪里。如果想要了解更多关于黑板模式的信息。我们常说的。Google是你最好的朋友。 一个最简单的黑板对象应该是 Dictionary一些简单的命名值的字典。所有的对象共享同一个字典引用。使得他们可以交换这些命名数据。这种方法有两个问题。一个是名字。一个是类型安全—数据生产者和消费者对每一个数据值都必须共享一个字符串标识。消费者也没有对字典中的值进行编译时的类型检查,比如,可能期望一个小数,结果运行时读到了字符串。本文对这两个问题演示了一种解决方案。 背景 最近我在开发一个通用任务的异步执行的引擎。我的通用任务通常有Do/Undo方法。原则上是相互独立的,但是有一些任务需要从已经执行的任务重请求数据。比如。一个任务可以 为一个硬件设备建立一个API,随后的任务就可以使用创建好的API来操作硬件设备。但是。我不想我的执行引擎知道关于这个执行任务的任何信息。而且。我也不想直接手工的就在一个任务里引用另一个任务。 黑板类 黑板类本质上是一个Dictionary的包装类,对外暴露Get和Set方法。黑板类允许其他对象存储并且取回数据。但是要求这些数据使用一个 BlackboardProperty 类型的标识符来表示这些数据是可存取的。BlackboardProperty 对象应该在那些准备读写黑板类的对象之间共享,因此,他应该在那些类中作为一个静态成员。(很像WPF的依赖属性。是他们所属控件的静态成员) 注意:命名安全应该可以通过同样的方式实现。但是但是依然没有解决类型安全的问题。那么。到了主要的部分了。那就是黑板类的代码了 public class Blackboard : INotifyPropertyChanged, INotifyPropertyChanging { Dictionary<string, object> _dict = new Dictionary<string, object>(); public T Get<T>(BlackboardProperty<T> property) { if (!_dict.ContainsKey(property.Name)) _dict[property.Name] = property.GetDefault(); return (T)_dict[property.Name]; } public void Set<T>(BlackboardProperty<T> property, T value) { OnPropertyChanging(property.Name); _dict[property.Name] = value; OnPropertyChanged(property.Name); } #region property change notification public event PropertyChangingEventHandler PropertyChanging; public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanging(string propertyName) { if (PropertyChanging != null) PropertyChanging(this, new PropertyChangingEventArgs(propertyName)); } protected virtual void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } #endregion } 黑板属性(BlackBoardProperty)类 BlackBoardProperty 类 提供了一个标识符来存取黑板对象中的数据。定义了名称和值的类型。也定义了一个默认的返回值。以防黑板类中对应属性没有值。 /// <summary> /// 对应黑板类中的属性的强类型标识符 /// </summary> /// <typeparam name="T">该类能识别的属性值的类型</typeparam> public class BlackboardProperty<T> { /// <summary> /// 属性的名称 /// <remarks> /// 黑板类的属性通过名称来存储。请注意不要让相同的名字有不同的属性值。因为如果被用在同样的黑板类上。他们会互相覆盖值 /// </remarks> /// </summary> public string Name { get; set; } //当黑板对象没有包含对应属性的时候。该工厂方法被用来提供一个默认的值 // Func<T> _createDefaultValueFunc; public BlackboardProperty(string name) : this(name, default(T)) { } /// <summary> /// /// </summary> /// <param name="name"></param> /// <param name="defaultValue"> /// 当黑板类不包括该属性的时候。该值会被返回。 /// <remarks> /// 如果缺省的值是一个常量或是一个值类型的时候,使用该构造方法。 /// </remarks> /// </param> public BlackboardProperty(string name, T defaultValue) { Name = name; _createDefaultValueFunc = () => defaultValue; } /// <summary> /// </summary> /// <remarks> /// 如果缺省值是一个引用类型,并且,你不想要共享该实例给多个黑板对象的时候。请使用该 /// 构造函数 /// </remarks> /// <param name="name"></param> /// <param name="createDefaultValueFunc"></param> public BlackboardProperty(string name, Func<T> createDefaultValueFunc) { Name = name; _createDefaultValueFunc = createDefaultValueFunc; } public BlackboardProperty() { Name = Guid.NewGuid().ToString(); } public T GetDefault() { return _createDefaultValueFunc(); } } 我承认不是非常有用的代码。但是。能够模拟两个类的使用。 下一个例子会更和现实情况接近。但是肯定是被简化过了的。在下面的例子里。我定义了集中不同的任务。我用这些任务来启动对硬件设备的连接。操作设备。关闭连接。这些任务通过一个执行引擎依次执行,这些任务通过一个公用的黑板类来共享数据。至于这个任务类的和执行引擎(ExecutionEngine)类还是留到另一篇文章中把。 ...

2012-10-16 · 3 min · bystander

如何创建WPF用户控件&在WPF项目中使用

作者给的Demo我合并了下。VS2010直接打开解决方案。二者都有。 介绍 本文展示在WPF中如何创建用户控件并且如果在WPF项目中使用。我将使用VS2008和C#来展示如何创建一个自定义的ToolTip 背景 这篇由Sacha Barber.写的和我的有点像。 使用代码 开始。首先,我们创建一个用户控件。因此,我们选择新建WPF用户控件类库(WPF User Control Library)。 现在。我们可以创建或者编辑XAML代码来创建自定义的用户控件了。我使用XAML来创建自定义的ToolTip。你想做什么随你。 <UserControl Name="UserControlToolTip" x:Class="CustomToolTip.UserControl1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" RenderTransformOrigin="0,0" HorizontalAlignment="Left" VerticalAlignment="Top" > <UserControl.RenderTransform> <TransformGroup> <ScaleTransform ScaleX="1" ScaleY="1"/> <SkewTransform AngleX="0" AngleY="0"/> <RotateTransform Angle="0"/> <TranslateTransform x:Name="UserControlToolTipXY" X="0" Y="0"/> </TransformGroup> </UserControl.RenderTransform> <Grid HorizontalAlignment="Center" VerticalAlignment="Center" MinWidth="200" MinHeight="120"> <Grid.RowDefinitions> <RowDefinition Height="0.333*"/> <RowDefinition Height="0.667*"/> </Grid.RowDefinitions> <Rectangle Fill="#FFFBFBFB" Stroke="#FF000000" RadiusX="10" RadiusY="10" RenderTransformOrigin="0.139,0.012" StrokeThickness="1" Grid.RowSpan="2"> <Rectangle.BitmapEffect> <DropShadowBitmapEffect Opacity="0.8"/> </Rectangle.BitmapEffect> </Rectangle> <Rectangle RadiusX="10" RadiusY="10" RenderTransformOrigin="0.139,0.012" StrokeThickness="10" Stroke="{x:Null}" Margin="1,1,1,1" Grid.Row="0" Grid.RowSpan="2"> <Rectangle.Fill> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0.725"> <GradientStop Color="#00E6D9AA" Offset="0.487"/> <GradientStop Color="#FFE6D9AA" Offset="0.996"/> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <Rectangle RadiusX="10" RadiusY="10" RenderTransformOrigin="0.493,0.485" StrokeThickness="10" Stroke="{x:Null}" Grid.RowSpan="2" Margin="1,1,1,1"> <Rectangle.Fill> <LinearGradientBrush EndPoint="0.014,0.5" StartPoint="0.211,0.5"> <GradientStop Color="#00E6D9AA" Offset="0.513"/> <GradientStop Color="#FFE6D9AA" Offset="0.996"/> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <Rectangle RadiusX="10" RadiusY="10" RenderTransformOrigin="0.493,0.485" StrokeThickness="10" Stroke="{x:Null}" Grid.RowSpan="2" Margin="1,1,1,1"> <Rectangle.Fill> <LinearGradientBrush EndPoint="0.493,0.002" StartPoint="0.493,0.33"> <GradientStop Color="#00E6D9AA" Offset="0.513"/> <GradientStop Color="#FFE6D9AA" Offset="0.996"/> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <Rectangle RadiusX="10" RadiusY="10" RenderTransformOrigin="0.493,0.485" StrokeThickness="10" Stroke="{x:Null}" Grid.RowSpan="2" Margin="1,1,1,1"> <Rectangle.Fill> <LinearGradientBrush EndPoint="0.99,0.441" StartPoint="0.794,0.441"> <GradientStop Color="#00E6D9AA" Offset="0.513"/> <GradientStop Color="#FFE6D9AA" Offset="0.996"/> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <TextBlock Text="TextBlock" TextWrapping="Wrap" x:Name="TextBlockToolTip" RenderTransformOrigin="0.5,0.5" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="20,0,0,20" /> <TextBlock Name="ToolTipTitle" HorizontalAlignment="Stretch" Margin="15,16,15,6.1" FontSize="14" Text="title" d:LayoutOverrides="Height" /> </Grid> </UserControl> 同时。我们需要添加一些方法和属性来控制这些元素。 namespace CustomToolTip { public partial class UserControl1 : UserControl { public UserControl1() { InitializeComponent(); } public double UserControlToolTipX { get { return this.UserControlToolTipXY.X; } set { this.UserControlToolTipXY.X = value; } } public double UserControlToolTipY { get { return this.UserControlToolTipXY.Y; } set { this.UserControlToolTipXY.Y = value; } } public string UserControlTextBlockToolTip { get { return TextBlockToolTip.Text; } set { TextBlockToolTip.Text = value; } } public string UserControlToolTipTitle { get { return ToolTipTitle.Text; } set { ToolTipTitle.Text = value; } } } } ...

2012-10-14 · 3 min · bystander

WPF绘制圆角多边形

介绍 最近,我发现我需要个圆角多边形。而且是需要在运行时从用户界面来绘制。WPF有多边形。但是不支持圆角。我搜索了一下。也没找到可行的现成例子。于是就自己做吧。本文描述了圆角多边形的实现,也包括如何用在你的项目里。在Demo里面的RoundedCornersPolygon 类是完整的实现。 下载的Demo包括两部分 1. 通过XAML绘制圆角多边形 2. 运行时创建圆角多边形 背景 多边形可以被认为是沿着一个给定半径的圆的边缘和一些指定点/边。所构成的点的集合。 在WPF中。你可以给Polygon对象的Points属性添加一系列的点来制作多边形。 XAML方式 <Canvas> <Polygon Points="10,50 180,50 180,150 10,150" StrokeThickness="1" Stroke="Black" /> </Canvas> C#方式 var cnv = new Canvas(); var polygon = new Polygon {StrokeThickness = 1, Fill = Brushes.Black}; polygon.Points.Add(new Point(10, 50)); polygon.Points.Add(new Point(180, 50)); polygon.Points.Add(new Point(180, 150)); polygon.Points.Add(new Point(10, 150)); cnv.Children.Add(polygon); this.Content = cnv; 上面两个例子会输出下面的矩形 使用代码 我写的RoundedCornersPolygon 类和普通的多边形类很相似。但是有更多的属性来控制圆角。首先。看一个例子。展示一下圆角矩形类的使用 XAML方式 <Canvas> <CustomRoundedCornersPolygon:RoundedCornersPolygon Points="10,50 180,50 180,150 10,150" StrokeThickness="1" Stroke="Black" ArcRoundness="25" UseAnglePercentage="False" IsClosed="True"/> <Canvas> C#方式 var cnv = new Canvas(); var roundedPolygon = new RoundedCornersPolygon { Stroke = Brushes.Black, StrokeThickness = 1, ArcRoundness = 25, UseAnglePercentage = false, IsClosed = true }; roundedPolygon.Points.Add(new Point(10, 50)); roundedPolygon.Points.Add(new Point(180, 50)); roundedPolygon.Points.Add(new Point(180, 150)); roundedPolygon.Points.Add(new Point(10, 150)); cnv.Children.Add(roundedPolygon); this.Content = cnv; 输出如下: 多边形有四个主要属性 ArcRoundness 属性指定了从距离LineSegment终点多远的距离开始弯曲,通常和UseRoundnessPercentage 一起使用。UseRoundnessPercentage属性指定了ArcRoundness 值是百分比还是一个固定的值。 举个例子。ArcRoundness 被设置成10,而且UseRoundnessPercentage 被设置成false,那么弯曲将会在距离线段终点10的地方开始。而如果UseRoundnessPercentage 被设置成ture。则会是从线段终点10%的地方开始弯曲。 /// <summary> /// Gets or sets a value that specifies the arc roundness. /// </summary> public double ArcRoundness { get; set; } /// <summary> /// Gets or sets a value that specifies if the ArcRoundness property /// value will be used as a percentage of the connecting segment or not. /// </summary> public bool UseRoundnessPercentage { get; set; } IsClosed 指定多边形的最后一个点是否和第一个点闭合。为了成为一个多边形。一般应该被设置为true ...

2012-10-13 · 3 min · bystander

一步步教你制作WPF圆形玻璃按钮

1.介绍 从我开始使用vista的时候,我就非常喜欢它的圆形玻璃按钮。WPF最好的一个方面就是允许自定义任何控件的样式。用了一段时间的Microsoft Expression Blend后。我做出了这个样式。我觉得做的还行。因为。我决定分享。如我所说。我使用Microsoft Expression Blend来做。但是。我也是用XAML编辑器–Kaxaml。 2.概述 玻璃按钮样式包含了三层。组织了玻璃效果(Glass Effect)和一个ContentPresenter 来存储按钮的内容。所有的这些层都在一个最外层的Grid里。当鼠标放到按钮上,按下去的时候也定义了一些触发器(Triggers),来增加一些交互。 我把这个样式做成了资源文件。但是这个Key可以删除,来使得所有的按钮都是这个效果。 好我们来看一下这些层次。这些被广泛应用在微软产品中的按钮。 **3.按钮层次 ** 3.1背景层 第一层是一个椭圆。其实是一个canvas,一会在上面画反射和折射层,填充的颜色和按钮的背景(Background)关联。 下面是Blend中的截图 图2 <!-- Background Layer --> <Ellipse Fill="{TemplateBinding Background}"/> 3.1.1折射层 第二层模拟了光从上到下的折射。被放在反射层之前是因为,要达到反光玻璃的效果,反射层必须在按钮的中间某处有一个硬边缘。这一层实际上是另一个椭圆。但是这次。我们使用一个径向渐变(白色-透明)的填充。来模拟光的折射。渐变开始于第一层底部的中央。结束于上面的中间。然而。为了降低折射光的强度。渐变还是开始于椭圆的底部再下一点为好。可以从图上和代码里清晰的看到。 <!-- Refraction Layer --> <Ellipse x:Name="RefractionLayer"> <Ellipse.Fill> <RadialGradientBrush GradientOrigin="0.496,1.052"> <RadialGradientBrush.RelativeTransform> <TransformGroup> <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1.5" ScaleY="1.5"/> <TranslateTransform X="0.02" Y="0.3"/> </TransformGroup> </RadialGradientBrush.RelativeTransform> <GradientStop Offset="1" Color="#00000000"/> <GradientStop Offset="0.4" Color="#FFFFFFFF"/> </RadialGradientBrush> </Ellipse.Fill> </Ellipse> 3.1.2反射层 第三层是光的反射层。是最难的部分。问题是反射效果不能使用任何标准的形状来画。因此。使用路径(Path)来画反射区域。当时。手工画也是可以的。但老实说。手工画图实在没什么可享受的(除非你是一个艺术家,或者有一个数位板),无论如何。我现在MS Blend中华好一个椭圆并转换成一个路径,然后我使用贝塞尔曲线点调整得到平滑的路径,你可以添加渐变到一个复杂的Path对象上。就像你对其他与定义的图形,比如椭圆,矩形所做的一样。为了得到光泽反射。我额每年需要一个透明-白色的径向渐变填充,从路径的底部开始(也就是按钮的中间某处),结束在顶部。我想如果我是一个艺术家。我会让渐变更准一点。可是我不是。因此。就这样。因为我们要把我们的按钮放在一个Grid里。所有我们设置VerticalAlignment=“Top” 这样反射区域在按钮的中间的结束了。 图三 <!-- Reflection Layer --> <Path x:Name="ReflectionLayer" VerticalAlignment="Top" Stretch="Fill"> <Path.RenderTransform> <ScaleTransform ScaleY="0.5" /> </Path.RenderTransform> <Path.Data> <PathGeometry> <PathFigure IsClosed="True" StartPoint="98.999,45.499"> <BezierSegment Point1="98.999,54.170" Point2="89.046,52.258" Point3="85.502,51.029"/> <BezierSegment IsSmoothJoin="True" Point1="75.860,47.685" Point2="69.111,45.196" Point3="50.167,45.196"/> <BezierSegment Point1="30.805,45.196" Point2="20.173,47.741" Point3="10.665,51.363"/> <BezierSegment IsSmoothJoin="True" Point1="7.469,52.580" Point2="1.000,53.252" Point3="1.000,44.999"/> <BezierSegment Point1="1.000,39.510" Point2="0.884,39.227" Point3="2.519,34.286"/> <BezierSegment IsSmoothJoin="True" Point1="9.106,14.370" Point2="27.875,0" Point3="50,0"/> <BezierSegment Point1="72.198,0" Point2="91.018,14.466" Point3="97.546,34.485"/> <BezierSegment IsSmoothJoin="True" Point1="99.139,39.369" Point2="98.999,40.084" Point3="98.999,45.499"/> </PathFigure> </PathGeometry> </Path.Data> <Path.Fill> <RadialGradientBrush GradientOrigin="0.498,0.526"> <RadialGradientBrush.RelativeTransform> <TransformGroup> <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1" ScaleY="1.997"/> <TranslateTransform X="0" Y="0.5"/> </TransformGroup> </RadialGradientBrush.RelativeTransform> <GradientStop Offset="1" Color="#FFFFFFFF"/> <GradientStop Offset="0.85" Color="#92FFFFFF"/> <GradientStop Offset="0" Color="#00000000"/> </RadialGradientBrush> </Path.Fill> </Path> 最后。我添加一个ContentPresenter 到按钮中间。经验告诉我,内容区域再向下一个像素会使得按钮看起来更漂亮。因此,在这里我用了margin属性(注意。因为内容区域在Grid的中间(Center)。所以2个像素的top实际上是向下移动了一个像素 ) 好了。最后在Blend中看起来大概是这样 图4 4.添加一些交互性 4.1鼠标悬停效果 为了有鼠标悬停效果,我们需要增加光源的亮度。因此。我们为IsMouseOver 事件定义一个触发器,复制并且粘贴反射和折射层的渐变设置代码。对于折射层。我仅仅移动了渐变的起点向上了一点。在反射层中。我改变了渐变停止点。使不透明的白色多一点。 <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="RefractionLayer" Property="Fill"> <Setter.Value> <RadialGradientBrush GradientOrigin="0.496,1.052"> <RadialGradientBrush.RelativeTransform> <TransformGroup> <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1.5" ScaleY="1.5"/> <TranslateTransform X="0.02" Y="0.3"/> </TransformGroup> </RadialGradientBrush.RelativeTransform> <GradientStop Offset="1" Color="#00000000"/> <GradientStop Offset="0.45" Color="#FFFFFFFF"/> </RadialGradientBrush> </Setter.Value> </Setter> <Setter TargetName="ReflectionLayer" Property="Fill"> <Setter.Value> <RadialGradientBrush GradientOrigin="0.498,0.526"> <RadialGradientBrush.RelativeTransform> <TransformGroup> <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1" ScaleY="1.997"/> <TranslateTransform X="0" Y="0.5"/> </TransformGroup> </RadialGradientBrush.RelativeTransform> <GradientStop Offset="1" Color="#FFFFFFFF"/> <GradientStop Offset="0.85" Color="#BBFFFFFF"/> <GradientStop Offset="0" Color="#00000000"/> </RadialGradientBrush> </Setter.Value> </Setter> </Trigger> ...

2012-10-12 · 2 min · bystander

自定义WPF LinkLabel 控件

WPF里是没有LinkLabel控件的。因此我自己写一个。首先。我们看一下WPF中什么类似的组件可以实现这个链接功能 如果你想要模拟一个LinkLabel控件。你可以在TextBlock里使用内联的Hyperlink。像下面这样 <TextBlock> <Hyperlink> <Run Text="Test link"/> </Hyperlink> </TextBlock> 你可以使用Label控件。加一个内联的HyperLink,但是我认为TextBlock更好。因为你可以在Expression Blend 中通过InlineCollection 编辑所有子元素的属性 图1 虽然这种方法也行,但是我还是不太喜欢。因为我觉得我还是写一个类似windows窗体程序中的LinkLabel控件。然后我就做了。首先看一下控件的样子 图2 第一个是默认的LinkLabel控件。第二个是LinkLabelBehavior 属性被设置为HoverUnderline ,第三个的Foreground和 HoverForeground 属性都使用了自定的颜色。 LinkLabel控件支持的属性 1.Foreground和 HoverForeground属性 允许自定义这两个属性的值 2.LinkLabelBehavior 属性 允许设置下划线的显示与否 3.自定义HyperlinkStyle 属性 你可以使用这个属性给超链接设置自定义的样式。如果你已经自定了Foreground和 HoverForeground。则会被覆盖。 Url 超链接的目标 所有这些属性都继承自标准的System.Windows.Controls.Label 控件 通过Blend/Xaml设置这些属性很简单 <ThemedControlLibrary:LinkLabel Content="Link Label" FontSize="22"/> <ThemedControlLibrary:LinkLabel Content="Link Label" LinkLabelBehavour="HoverUnderline" /> <ThemedControlLibrary:LinkLabel Foreground="#FF847901" HoverForeground="#FF06C8F2" Content="Link Label" LinkLabelBehavour="NeverUnderline"/> 图三 然后是控件的使用方法。仅仅添加命名空间到xaml中。然后使用就行了。 <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="DemoApplication.Window1" Title="DemoApplication" Height="300" Width="300" xmlns:ThemedControlsLibrary="clr-namespace:ThemedControlsLibrary;assembly=ThemedControlsLibrary" > <Grid> <ThemedControlsLibrary:LinkLabel HorizontalAlignment="Left" VerticalAlignment="Top" Content="LinkLabel"/> </Grid> </Window> 控件的完整代码很简单。就定义一下需要的属性,和控制这些属性应该显示在Blend中的(category)目录位置就行了。 public class LinkLabel : Label { private const string _linkLabel = "LinkLabel"; public static readonly DependencyProperty UrlProperty = DependencyProperty.Register("Url", typeof(Uri), typeof(LinkLabel)); [Category("Common Properties"), Bindable(true)] public Uri Url { get { return GetValue(UrlProperty) as Uri; } set { SetValue(UrlProperty, value); } } public static readonly DependencyProperty HyperlinkStyleProperty = DependencyProperty.Register("HyperlinkStyle", typeof(Style), typeof(LinkLabel)); public Style HyperlinkStyle { get { return GetValue(HyperlinkStyleProperty) as Style; } set { SetValue(HyperlinkStyleProperty, value); } } public static readonly DependencyProperty HoverForegroundProperty = DependencyProperty.Register("HoverForeground", typeof(Brush), typeof(LinkLabel)); [Category("Brushes"), Bindable(true)] public Brush HoverForeground { get { return GetValue(HoverForegroundProperty) as Brush; } set { SetValue(HoverForegroundProperty, value); } } public static readonly DependencyProperty LinkLabelBehavourProperty = DependencyProperty.Register("LinkLabelBehavour", typeof(LinkLabelBehaviour), typeof(LinkLabel)); [Category("Common Properties"), Bindable(true)] public LinkLabelBehaviour LinkLabelBehavour { get { return (LinkLabelBehaviour)GetValue(LinkLabelBehavourProperty); } set { SetValue(LinkLabelBehavourProperty, value); } } static LinkLabel() { FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata( typeof(LinkLabel), new FrameworkPropertyMetadata(typeof(LinkLabel))); } } ...

2012-10-11 · 2 min · bystander

C#编写FTP客户端软件

1 介绍 我知道。网上有很多现成的FTP软件。但是。我们也想要了解FTP的一些底层机构,因此。 这个开源的项目在你学习FTP知识的时候也许对你有些帮组。程序的界面看起来像FileZilla,FileZilla虽然流行但是有些bug,当我打开我博客的时候总是有问题。我需要通过FTP连接我的服务器。发送文件,下载文件等等。因为。我决定写我自己的软件来处理所有的情况。FileZilla足够好。但它不是我的。 2 背景 看看我们已经知道的。我们知道FTP是一个标准的基于TCP网络协议。用于从一个主机向另一个主机传输文件。它是一个C/S架构。 图2 FTP程序曾经是基于命令行的。我们仍沿可以通过cmd.exe连接FTP服务器。因为FTP的确可以通过命令来操作。举个例子。我们可以在命令行使用“stor”命令来发送文件。为了完成这些请求。FTP服务器需要一直运行等待即将到来的客户端请求。我们可以从来自维基百科的解释更好的理解FTP: 客户端计算机可以通过服务器的21端口和服务器通信。叫做控制连接。它在一次会话期间保持开放。第一次连接的时候。叫做数据连接,服务器可以对客户端打开20端口(主动模式),建立一条数据通路,连接上客户端传输数据。或者客户端打开一个随机的端口(被动模式),去连接服务器,来传输数据。控制连接使用一个类似Telnet的协议,被用作客户端和服务器会话管理(命令,标识,密码)。。比如。“RETR filename” 会从服务器端下载文件。 图三 一个完整的FTP文件传输需要建立两种类型的连接,一种为文件传输下命令,称为控制连接,另一种实现真正的文件传输,称为数据连接。 服务器 通过三位ASCII的数字状态码,可能包含可选的描述信息,在控制连接上做出回应。比如。“200”或者是“200 OK”,表示上一条命令成功了。数字代表编号,描述信息给出了一些说明(比如“OK”),或者可能是一些需要的参数(比如需要帐号来存储文件),那么我们需要怎么做呢。很明显。发送命令,接收“OK”回应,发送数据。接收数据。完了。但是首先需要服务器已经准备好了。FTP服务器可以在主动和被动两种模式下运行。主动模式是基于服务器的连接而被动模式是基友客户端的连接。继续看。 在主动连接中,客户端把自己的ip和端口发送给服务器。然后服务器尝试连接到客户端,但是可能会因为防火墙的原因而被拒绝。我们在windows上都会使用反病毒/自带防火墙。是吧。那么我们来看看被动模式 在被动连接中。服务器通过一个“PASV”命令把自己的ip和端口发送给客户端。然后客户端通过该IP尝试连接服务器。对于发送文件非常有用。当我们发送文件的时候。优先使用“PASV”模式,如你们所说。大多数协议。像FTP/HTTP 使用ASCII编码,因为全球可用。因此我们会使用这种编码。你可以从下面得到FTP的命令列表 主动和被动都是对于服务器端来说的 3 使用代码 现在我们已经为编写软件做好准备了。我们写些有用的代码吧。:)首先。我们“打开文件对话框”,集成到我们的窗体里。 3.1 资源管理器组件 我们需要一个资源管理器组件在软件界面可以看到我们所有的文件。这样我们才可以选择哪些文件来发送到FTP服务器,新建一个Windows窗体控件库(下载包中提供了) 图四 最后看起来样子是上面这样。先添加一个TreeView,一些按钮,和一个搜索功能 TreeView.Nodes.Clear(); TreeNode nodeD = new TreeNode(); nodeD.Tag = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); nodeD.Text = “Desktop”; nodeD.ImageIndex = 10; nodeD.SelectedImageIndex = 10; TreeView.Nodes.Add(nodeD); 就像上面代码展示的那样。我们需要添加地一个主节点。我的文档。我的电脑等等。然后获得子目录。 string[] dirList; dirList = Directory.GetDirectories(parentNode.Tag.ToString()); Array.Sort(dirList); if (dirList.Length == parentNode.Nodes.Count) return; for (int i = 0; i < dirList.Length; i++) { node = new TreeNode(); node.Tag = dirList[i]; node.Text = dirList[i].Substring(dirList[i].LastIndexOf(@"\") + 1); node.ImageIndex = 1; parentNode.Nodes.Add(node); } 可以从下载包里看到完整的代码。我们还应该处理鼠标单击事件。 现在我们有了一个资源管理器。还有FTP和VS所需要的所有信息。 首先,我们连接服务器。我们应该怎么做呢? FTPSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); AppendText(rchLog,"Status : Resolving IP Address\n",Color.Red); remoteAddress = Dns.GetHostEntry(Server).AddressList[0]; AppendText(rchLog, "Status : IP Address Found ->" + remoteAddress.ToString() + "\n", Color.Red); addrEndPoint = new IPEndPoint(remoteAddress, Port); AppendText(rchLog,"Status : EndPoint Found ->" + addrEndPoint.ToString() + "\n", Color.Red); FTPSocket.Connect(addrEndPoint); 是的。我们需要一个socket连接到服务器 ,然后发送命令 AppendText(rchLog, "Command : " + msg + "\n", Color.Blue); Byte[] CommandBytes = Encoding.ASCII.GetBytes((msg + "\r\n").ToCharArray()); FTPSocket.Send(CommandBytes, CommandBytes.Length, 0); //read Response ReadResponse(); ...

2012-10-09 · 2 min · bystander