WPF对象都具有RenderTransform的属性,可以通过设置RenderTransform来对WPF的元素进行变换,无论是控件还是形状都可以变换。典型的变换包括缩小放大与平移。
(一)缩放
(二)平移
为了实现平移,这里以按下鼠标中间键并移动鼠标作为事件触发方式,来实现平移。即先下辖鼠标中键(滚轮键),移动鼠标,这样WPF元素就会跟随鼠标平移。
WPF元素和形状的变换是针对于自身的坐标系的,因此不能直接获取其自身坐标系的点来进行变换,否则是不对了,有时也会不停闪烁跳动,本人一开始不小心用错了,就用待平移的WPF元素自身坐标点来作为平移参数,后来发现屏幕不停闪烁跳动,且平移距离也不正确。
如果采用Canvas作为画板来绘制一些形状,想要通过鼠标或触摸操作来进行平移,那么不能简单地对canvas进行变换,否则Cancas平移的时候就会覆盖周边的其它控件,也就是Canvas画布自身被移动了,而不仅仅是Canvas内部画出来的形状被移动了。
那如果需要实现移动怎么办呢?与前一篇文章一样,我采取的方式是在Canvas外面包装一个WPF元素,比如Border元素。这样,Canvas就成为Border元素的子元素了,然后在Border元素上实现鼠标控制操作来变换Canvas元素。
<Border Name="outside" Grid.Column="1" Background="LightBlue"
PreviewMouseDown="outsidewrapper_PreviewMouseDown"
PreviewMouseMove="outsidewrapper_PreviewMouseMove"
PreviewMouseUp="outside_PreviewMouseUp"
PreviewMouseWheel="outside_PreviewMouseWheel"
ClipToBounds="True">
<Canvas Name="inside" Width="{Binding Path=ActualWidth,RelativeSource={RelativeSource AncestorType=Border}}"
Height="{Binding Path=ActualHeight,RelativeSource={RelativeSource AncestorType=Border}}">
<Canvas.RenderTransform>
<TransformGroup/>
</Canvas.RenderTransform>
<Line Canvas.Left="50" Canvas.Top="50" X1="100" Y1="200" X2="100" Y2="200" Stroke="Black" StrokeThickness="5"/>
<Rectangle Canvas.Left="150" Canvas.Top="150" Width="380" Height="296" Fill="Red" />
</Canvas>
</Border>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
Point previousPoint;
bool isTranslateStart = false;
private void outsidewrapper_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
//说明:虽然WPF元素存在MouseLeftButtonDown和MouseRightButtonDown事件,
//但却没找到MouseMiddleButtonDown事件。
//然而,幸运的是,MouseDown事件是不论哪个鼠标键按下的时候都会触发的,因此可以利用该事件,
//并通过检测哪个键被按下的状态参数来判断是否是中间键被按下了。
//也就是说,当出现MouseDown事件的时候,判断三个键的状态,如果中间键被按下,
//而其它两个键没有被按下,则认为是中间键按下的事件。在该事件中,记录下当时鼠标的位置
if(e.MiddleButton==MouseButtonState.Pressed&&e.LeftButton==MouseButtonState.Released&&e.RightButton==MouseButtonState.Released)
{
previousPoint = e.GetPosition(outside);
isTranslateStart = true;
}
e.Handled = true;
}
private void outsidewrapper_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.MiddleButton == MouseButtonState.Pressed && e.LeftButton == MouseButtonState.Released && e.RightButton == MouseButtonState.Released)
{
if (isTranslateStart)
{
Point currentPoint = e.GetPosition(outside); //不能用 inside,必须用outside
Vector v = currentPoint - previousPoint;
TransformGroup tg = inside.RenderTransform as TransformGroup;
tg.Children.Add(new TranslateTransform(v.X,v.Y)); //centerX和centerY用外部包装元素的坐标,不能用内部被变换的Canvas元素的坐标
// inside.RenderTransform = tg;
previousPoint = currentPoint;
}
}
e.Handled = true;
}
private void outside_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (e.MiddleButton == MouseButtonState.Pressed && e.LeftButton == MouseButtonState.Released && e.RightButton == MouseButtonState.Released)
{
if (isTranslateStart)
{
isTranslateStart = false;
}
}
e.Handled = true;
}
private void outside_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
Point currentPoint = e.GetPosition(outside); //不能用 inside,必须用outside
TransformGroup tg = inside.RenderTransform as TransformGroup;
double s = ((double)e.Delta) / 1000.0+1.0;
//centerX和centerY用外部包装元素的坐标,不能用内部被变换的Canvas元素的坐标
tg.Children.Add(new ScaleTransform(s,s,currentPoint.X,currentPoint.Y));
e.Handled = true;
}
}
需要注意事项包括:
(1)包装的元素需要添加ClipToBounds="True"属性,这样内部Canvas超出包装元素的时候,超出部分就会被裁剪掉;
(2)把Canvas元素的初始大小设置为与包装元素一样大小,可以通过RelativeSource来设置:Width="{Binding Path=ActualWidth,RelativeSource={RelativeSource AncestorType=Border}}" ,Height="{Binding Path=ActualHeight,RelativeSource={RelativeSource AncestorType=Border}}"。
(3)将外包包装元素的背景和Canvas的背景设置为一样的背景,这样当Canvas平移的时候,不会给然感觉Canvas画布在移动,而是感觉内部元素(比如线、多边形、圆形、矩形、文字等等)。或者Canvas不设置背景也是可行的(但可能Canvas会收不到鼠标事件......)。
(4)用鼠标事件控制平移的话,可以使用隧道事件,且事件应该关联在外部包装元素Border上。平移的大小用外包装元素坐标系统中的坐标点进行计算。当然具体根据需要采用合适的事件,比如滚轮事件之类的,触控事件之类的。
(5)虽然WPF元素存在MouseLeftButtonDown和MouseRightButtonDown事件,但却没找到MouseMiddleButtonDown事件。然而,幸运的是,MouseDown事件是不论哪个鼠标键按下的时候都会触发的,因此可以利用该事件,并通过检测哪个键被按下的状态参数来判断是否是中间键被按下了。也就是说,当出现MouseDown事件的时候,判断三个键的状态,如果中间键被按下,而其它两个键没有被按下,则认为是中间键按下的事件。在该事件中,记录下当时鼠标的位置。鼠标移动的时候,获取新的鼠标位置,并与记录为对象成员变量的前一个鼠标位置相减,即刻得知平移量,通过向Canvas的TransformGroup中添加平移变换,即可实现平移变换。
(6)理解:按理说所有元素的变换都是针对自身的坐标体系的,而不是针对外部父元素的坐标体系的,但是WPF有个特点,虽然时针对自身坐标系的变化,但是变换前后自身的ActualWidth和ActualHeight都没有发生任何变化,下次继续变换的时候,还是用的ActualWidth和ActualHeigth作为自身坐标系的参考用途,而不是按照变换后的实际尺寸在定位自身坐标系尺寸的。所以,使用外部包装元素的坐标系来给定每次变换的(CenterX和CenterY)是可行的。这一点需要慢慢理解。