用Silverlight打造位运算器(1)--制作简易工具条控件
那日,一网友发了一份他做破解计算器源码给我,看到这个程序,我突然想起了我写的第一个程序――位运算器,那是一个半途而废的程序。当时已经写了一千多行的代码,由于初学,里面有大量的重复代码,估计写完得用3000行以上的代码,而且大部是重复代码,这让我很困惑,于是放弃了继续完成它,转而寻找解除困惑的途径。之后,随着学习的深入,虽然这种困惑早已不复存在,但位运算器也消失在我的记忆之中了。网友的程序让我有了完成这个位运算器的念头,或许因为它是我写的第一个程序,在我的内心深处,多少还存留有一些对它的眷顾吧。
说做就做,接下来就要考虑使用什么语言来实现它,用C#的WindowApplication来写当然没问题,但那样学不到什么新东西,亏本的买卖不能做。Silverlight之前虽然学过一些,也做过一些东西,但说实话,还属于初学者,正好可以借此机会进一步了解Silverlight。
位运算器在我的构思中需要有工具条按钮,就是按下去不会弹上来那种按钮。当年Delphi中有一个SpeedButton控件正好可以实现这样的功能。C#的WindowsApplication中可以使用RadioButton来实现这样的工具条。很遗憾,Silverlight中没有相关控件。但可以更改ListBox的模板来模拟这样的功能,这个我试验过,但由于ListBox存在Focus状态,ListItem在处于Focus状态时会有一个边框,看着很不顺眼,需要重写模板把这个状态去掉。定制RadioButton外观应该也能实现这样的功能(这点未做实验,有待验证)。但想来想去,为了学些新东西,还是自己实现一个工具条控件吧。经过一番努力,终于实现了这个工具条。本文将对制作的步骤进行详细讲解。
好,先来看看效果,这里需要注意,必须安装Silverlight 3.0版本才能正常运行示例。
关于控件制作,请先观看一篇写得非常棒的文章:
http://msdn.microsoft.com/zh-cn/magazine/cc721611.aspx
由于我的控件放在另一个项目之中,操作步骤会有所不同,所以这里有些地方我还得重新详细地介绍。
第一步,先做一个最简单的控件:
1、 首先创建一个Silverlight应用程序,项目命名为“BitCalculator”。
2、 在解决方案资源管理器的【解决方案“BitCalculator”】上单击鼠标右键,选中【添加】à【新建项目】添加一个【Silverlight类库】,命名为“BitLibrary”。
3、 将【BitLibrary】项目下的“Class1.cs”类改名为“SpeedButton.cs”。
4、 将SpeedButton.cs类的代码更改如下:
2 {
3 public class SpeedButton : ContentControl
4 {
5
6 }
7 }
8
这里,我们让SpeedButton类继续自ContentControl,道理很简单,Button类也是从ContentControl类继续的。很多的拥有Content属性的内置控件都继承自ContentControl类。我们记得,在C#中,Button使用的是Text属性,为什么在Silverlight中会改为Content属性呢?这是因为Content属性允许用户对按钮表面的内容进行自定义,如加一些图片或其他控件什么的,而所有这些都不会影响按钮被单击。
5、 现在这个最简单的控件已经完成,可以加载到主页面测试一下了。打开【BitCalculator】项目下的【MainPage.xaml】文件,首先添加对这个控件的引用:
xmlns:src="clr-namespace:BitLibrary;assembly=BitLibrary"
这里的“src”是一个前缀,也可以把它理解为引用名,你高兴的话可以换成其他的单词。之后所有需要使用到SpeedButton的地方都需要加上“src:”前缀。
(这一段是我介绍的那篇文章未描述步骤)还需要做一件事才能在MainPage.xaml中看到SpeedButton:在【BitCalculator】项目上单击鼠标右键,选中【添加引用】打开“添加引用”窗口,在【项目】栏中选中“BitLibrary”,单击【确定】按钮添加对BitLibrary的引用。
接下来,在MainPage.xaml中加入SpeedButton控件:
<src:SpeedButton/>
完成后,MainPage.xaml的所有代码应该象下面这个样子:
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"
xmlns:src="clr-namespace:BitLibrary;assembly=BitLibrary"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">
<src:SpeedButton/>
</Grid>
</UserControl>
6、运行程序,我们发现浏览器中无任何内容,这很正常,我们的SpeedButton里面什么都没有。
第二步,修改控件模板:
我们首先看看使用常规方法是否能让SpeedButton显示一些内容:在【MainPage.xaml】中更改SpeedButton的声明如下:
Foreground="#FF000000" Content="HEX"
BorderBrush="Black" BorderThickness="2"/>
运行程序,我们发现除了Content里的“HEX“这几个字母外,其他的背景及边框根本没有按照我们的设想显示出来。这时,可以通过创建默认控件模板指定控件外观。
1、 在【BitLibrary】项目上单击鼠标右键,选择【添加】à【新建文件夹】添加一个新的文件夹,并把文件夹命名为“themes”。
2、 在【themes】文件夹上单击鼠标右键,选择【添加】à【新建项】,新建一个文本文件,并命名为:“generic.xaml”。这里需要注意,文件夹和文件的名称都不能搞错。
3、 打开generic.xaml文件,并填入如下代码:
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:BitLibrary;assembly=BitLibrary"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
<!-- SpeedButton Template -->
<Style TargetType="src:SpeedButton">
<Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Background" Value="#FF1F3B53"/>
<Setter Property="Foreground" Value="#FF000000"/>
<Setter Property="Padding" Value="3"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA3AEB9" Offset="0"/>
<GradientStop Color="#FF8399A9" Offset="0.375"/>
<GradientStop Color="#FF718597" Offset="0.375"/>
<GradientStop Color="#FF617584" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="src:SpeedButton">
<Grid>
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal"/>
<vsm:VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Duration="0" Storyboard.TargetName="BackgroundAnimation" Storyboard.TargetProperty="Opacity" To="1"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#F2FFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#CCFFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#7FFFFFFF"/>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="Background" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#FF6DBDD1"/>
<DoubleAnimation Duration="0" Storyboard.TargetName="BackgroundAnimation" Storyboard.TargetProperty="Opacity" To="1"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" To="#D8FFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#C6FFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#8CFFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#3FFFFFFF"/>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Border x:Name="Background" CornerRadius="3" Background="White" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
<Grid Background="{TemplateBinding Background}" Margin="1">
<Border Opacity="0" x:Name="BackgroundAnimation" Background="#FF448DCA" />
<Rectangle x:Name="BackgroundGradient" >
<Rectangle.Fill>
<LinearGradientBrush StartPoint=".7,0" EndPoint=".7,1">
<GradientStop Color="#FFFFFFFF" Offset="0" />
<GradientStop Color="#F9FFFFFF" Offset="0.375" />
<GradientStop Color="#E5FFFFFF" Offset="0.625" />
<GradientStop Color="#C6FFFFFF" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</Border>
<ContentPresenter
x:Name="contentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
这段代码其实是从Button的模板直接拷贝过来的,这里只是稍作修改,把用不上的东西去掉,如Focus状态,Button模板可以在Silverlight的帮助文档中获取。我懒得去重新设计外观了,将就用一下吧。
4、 打开SpeedButton.cs文件,给SpeedButton类添加构造函数如下:
2 {
3 public SpeedButton()
4 {
5 DefaultStyleKey = typeof(SpeedButton);
6 }
7 }
8
5、 修改MainPage.xaml文件,更改SpeedButton的声明如下:
6、运行程序,现在一个完整钮呈现在我们的面前,如下图所示。
当我们把鼠标指针移到按钮上方或按下按钮时无任何反应,这时需要对鼠标的事件进行处理,指定这些事件的可视状态。
第三步,给按钮添加鼠标响应事件:
虽然我们给按钮设定了模板,并在模板中指定了鼠标经过及按钮按下时的状态,但我们并没有在SpeedButton中指定何时呈现这些状态。下面处理SpeedButton的鼠标事件,并指定相应的可视状态。
打开SpeedButton.cs文件,并修改代码如下:
2 {
3 public SpeedButton()
4 {
5 DefaultStyleKey = typeof(SpeedButton);
6 //指定鼠标事件方法
7 MouseEnter += new MouseEventHandler(SpeedButton_MouseEnter);
8 MouseLeave += new MouseEventHandler(SpeedButton_MouseLeave);
9 MouseLeftButtonDown += new MouseButtonEventHandler(SpeedButton_MouseLeftButtonDown);
10 }
11 private Boolean isPress = false; //指示按钮是否处于按下状态
12 public Boolean IsPress
13 {
14 get
15 {
16 return isPress;
17 }
18 set
19 {
20 if (IsPress == value) return;
21 isPress = value;
22 if (isPress)
23 { //将按钮显示为模板中的Pressed可视状态。
24 VisualStateManager.GoToState(this, "Pressed", true);
25 }
26 else
27 {
28 VisualStateManager.GoToState(this, "Normal", true);
29 }
30 }
31 }
32 void SpeedButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
33 {
34 IsPress = !IsPress;
35 }
36
37 void SpeedButton_MouseLeave(object sender, MouseEventArgs e)
38 {
39 if (!isPress)
40 {
41 VisualStateManager.GoToState(this, "Normal", true);
42 }
43 }
44
45 void SpeedButton_MouseEnter(object sender, MouseEventArgs e)
46 {
47 if (!isPress)
48 {
49 VisualStateManager.GoToState(this, "MouseOver", true);
50 }
51 }
52 }
53
运行程序,用鼠标单击按钮看看有何不同。至此,一个简单的SpeedButton制作完毕。
第四步,制作ToolBar控件:
工具按钮是有了,接下来开始制作工具条。工具条其实就是一个容器,里面存放着几个工具按钮,当单击其中一个按钮时,另一个处于按下状态的按钮会自动弹上来,并且可以随时知道工具条当前按下的是哪个按钮。工具条的技术难题在于,当点击按钮改变工具条的选中项时,工具条如何知道按钮被点击,并触发一个事件来通知用户呢?按钮虽然拥有MouseLeftButtonDown事件,但如何把这个事件传递给工具条呢?
最初的想法是制作一个通用的工具条,但说实话,水平还不够,Silverlight中的很多机制还未了解透彻。而且通用控件需要考虑的东西太多,光代码可能就比位运算器多很多。这次还是制作一个位运算器专用的控件吧,好处是代码非常少,也容易理解。等未来水平达到了再考虑制作通用控件吧。
1、在【BitLibrary】项目上单击鼠标右键,选择【添加】à【新建项】,添加一个Silverlight用户控件,并命名为“ToolBar.xaml”。更改ToolBar.xaml文件如下:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:BitLibrary;assembly=BitLibrary">
<StackPanel x:Name="LayoutRoot" Height="30" Width="120" Loaded="LayoutRoot_Loaded"
Orientation="Horizontal"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<src:SpeedButton x:Name="btnBin" Content="BIN"/>
<src:SpeedButton x:Name="btnOct" Content="OCT"/>
<src:SpeedButton x:Name="btnBcd" Content="BCD"/>
<src:SpeedButton x:Name="btnHex" Content="HEX"/>
</StackPanel>
</UserControl>
ToolBar控件只是由一个StackPanel装载了四个SpeedButton控件,并且每个按钮都命了名,以方便在后台代码文件中调用。
接下来在Loaded="LayoutRoot_Loaded"上单击鼠标右键,在弹出菜单中选择【导航到事件处理程序】,这时自动打开ToolBar.xaml.cs文件,并自动生成了LayoutRoot_Loaded事件方法。
2、打开MainPage.xaml文件,并将<Grid>标签内的控件改为ToolBar:
<src:ToolBar/>
运行程序,这时带有四个SpeedButton的工具条呈现,单击每个按钮,可以发现当按下一个按钮时,其他按钮不会自动弹起。下面着手解决这个问题。
第五步,让工具条内的按钮互斥:
1、工具条内的四个按钮分别代表四种状态:
BIN:二进制
OCT:八进制
BCD:十进制
HEX:十六进制
为了方便后面写程序,这里声明一个枚举来表示这四种状态。
在【BitLibrary】项目上单击鼠标右键,选择【添加】à【新建项】,生成一个新类,命名为:“SystemState.cs”。在其中输入如下代码:
2 {
3 None,
4 Bin,
5 Oct,
6 Bcd,
7 Hex
8 }
9
2、给按钮声明一个SystemState属性,用于保存这个按钮所属状态。打开SpeedButton.cs文件,并在SpeedButton添加一个State属性:
2 {
3 get;
4 set;
5 }
3、打开ToolBar.xaml.cs文件,更改代码如下:
2 { //声明一个路由事件SelectionChange,用于在状态改变时通知用户
3 public event RoutedEventHandler SelectionChange;
4 public ToolBar()
5 {
6 InitializeComponent();
7 }
8
9 private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
10 { //设置每个按钮所属状态
11 btnBin.State = SystemState.Bin;
12 btnOct.State = SystemState.Oct;
13 btnBcd.State = SystemState.Bcd;
14 btnHex.State = SystemState.Hex;
15 //将每个按钮的MouseLeftButtonDown事件连接至ToolBar的btn_MouseLeftButtonDown方法之中
16 btnBin.MouseLeftButtonDown += new MouseButtonEventHandler(btn_MouseLeftButtonDown);
17 btnOct.MouseLeftButtonDown += new MouseButtonEventHandler(btn_MouseLeftButtonDown);
18 btnBcd.MouseLeftButtonDown += new MouseButtonEventHandler(btn_MouseLeftButtonDown);
19 btnHex.MouseLeftButtonDown += new MouseButtonEventHandler(btn_MouseLeftButtonDown);
20 }
21 private SystemState toolState = SystemState.None;
22 public SystemState ToolState //表示工具条当前所属状态
23 {
24 get { return toolState; }
25 set
26 {
27 if (toolState == value) return;
28 toolState = value;
29 //恢复每个按钮的正常状态
30 btnBin.IsPress = false;
31 btnOct.IsPress = false;
32 btnBcd.IsPress = false;
33 btnHex.IsPress = false;
34 //根据当前状态使相应按钮处于按下状态
35 switch (toolState)
36 {
37 case SystemState.Bin:
38 btnBin.IsPress = true;
39 break;
40 case SystemState.Oct:
41 btnOct.IsPress = true;
42 break;
43 case SystemState.Bcd:
44 btnBcd.IsPress = true;
45 break;
46 case SystemState.Hex:
47 btnHex.IsPress = true;
48 break;
49 }
50 //触发SelectionChange事件
51 if (SelectionChange != null)
52 {
53 SelectionChange(this, new RoutedEventArgs());
54 }
55 }
56 }
57 //所有按钮单击时都会连接至这个方法
58 void btn_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
59 { //通过设置ToolState属性以执行set访问器代码
60 this.ToolState = ((SpeedButton)sender).State;
61 }
62 }
63
这里我们把所有按钮的单击事件连接至ToolBar类里的btn_MouseLeftButtonDown()方法之中,这样按钮的单击事件就可以传递至ToolBar工具条了。呵呵,这一招是看ToolKit源码时学到的,这方法够巧妙。
现在可以运行程序,可以发现,当按下一个按钮时,之前被按下的按钮会自动弹起。
4、为了测试我们按下按钮时是否可以马上知道工具条当前状态,更改MainPage.xaml代码如下:
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"
xmlns:src="clr-namespace:BitLibrary;assembly=BitLibrary"
mc:Ignorable="d" d:DesignWidth="120" d:DesignHeight="70">
<StackPanel x:Name="LayoutRoot">
<src:ToolBar x:Name="toolBar" Grid.Row="1" SelectionChange="toolBar_SelectionChange"/>
<TextBlock x:Name="txtMsg" Width="120" TextAlignment="Center" FontSize="13" Margin="10"/>
</StackPanel>
</UserControl>
打开MainPage.xaml.cs文件,并更改代码如下:
2 {
3 public MainPage()
4 {
5 InitializeComponent();
6 }
7
8 private void toolBar_SelectionChange(object sender, RoutedEventArgs e)
9 {
10 txtMsg.Text = "你按下了" + toolBar.ToolState.ToString() + "按钮";
11 }
12 }
现在就完成了如文章开头所演示的效果。待续。。。。。。