WPF中的图表设计器 – 2
[原文地址] http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part2.aspx
[原文作者] sukram
[译者]WizRay
- Download source – 285.4 KB (requires .NET 3.5 SP1)
介绍
在上一篇文章中,我们展示了如何移动、缩放和旋转一个Canvas中的对象。这次,我们要加入一些典型的图表编辑器所必不可少的更深一层的特性:
- Designer Canvas(可变大小、可缩放)
- Zoombox
- 框选
- 键盘支持(Ctrl + 鼠标左键)
- Toolbox(可拖动)
- 可旋转组件
Designer Canvas
在上一篇文章中,你大概已经注意到了,当你把一个元素拖动到DesignerCanvas边框以外时,它就无法再访问到了。通常,用户期望设计器能够提供一个滚动条,使得用户可以将工作区移动到画布可视范围以外的任何区域。为此,我不得不吧DesignerCanvas移动到一个ScrollViewer里面,不过这没有用。很快,我知道了失效的原因,先让我来解释下面这些代码段:
<Canvas Width="200" Height="200" Background="WhiteSmoke"> <Rectangle Fill="Blue" Width="100" Height="100" Canvas.Left="300" Canvas.Top="300" /> </Canvas>
我把一个Rectangle放置在一个Canvas中,但是把它放在Canvas的边界以外。这显然不会改变Canvas的尺寸,不管我们把那个Rectangle放在哪儿。
这一位置对于DesignerCanvas,不论我们把一个对象移到离它的边界多么远的地方,他都不会改变尺寸。这样我们就理解了为什么ScrollView在这儿不管用:DesignerCanvas永远不会通知ScrollViewer它的尺寸发生变化,因为它根本就没有变化。
解决方案是我们必须强制设定DesignerCanvas的尺寸随时与移动或缩放的元素保持适应。幸运的是,Canvas提供了名为MeasureOverride的方法,这个方法能够允许DesignerCanvas计算它所需的尺寸,并将结果返回WPF的布局系统。这种计算很简单,就像下面这样:
protected override Size MeasureOverride(Size constraint) { Size size = new Size(); foreach (UIElement element in base.Children) { double left = Canvas.GetLeft(element); double top = Canvas.GetTop(element); left = double.IsNaN(left) ? 0 : left; top = double.IsNaN(top) ? 0 : top; //measure desired size for each child element.Measure(constraint); Size desiredSize = element.DesiredSize; if (!double.IsNaN(desiredSize.Width) && !double.IsNaN(desiredSize.Height)) { size.Width = Math.Max(size.Width, left + desiredSize.Width); size.Height = Math.Max(size.Height, top + desiredSize.Height); } } //for aesthetic reasons add extra points size.Width += 10; size.Height += 10; return size; }
DesignerItem
DesignerItem是从ContentControl继承下来的,所以它能够重用上一篇文章中的ControlTemplate。DesignerItem提供了IsSelected属性来表示他是否被选中:
public class DesignerItem : ContentControl { public bool IsSelected { get { return (bool)GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register("IsSelected", typeof(bool), typeof(DesignerItem), new FrameworkPropertyMetadata(false));
而后我们实现了MouseDown的事件处理来支持多选:
protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { base.OnPreviewMouseDown(e); DesignerCanvas designer = VisualTreeHelper.GetParent(this) as DesignerCanvas; if (designer != null) { if ((Keyboard.Modifiers & (ModifierKeys.Shift | ModifierKeys.Control)) != ModifierKeys.None) { this.IsSelected = !this.IsSelected; } else { if (!this.IsSelected) { designer.DeselectAll(); this.IsSelected = true; } } } e.Handled = false; }
注意到我们对PreviewMouseDown事件进行监听,并且我们设定该事件没有被处理过。这是因为我们希望即使MouseDown指向了DesignerItem里的一个子级元素,这个DesignerItem也能被选中;就像在Visual Studio的类设计器中,如果我们点击了Expander的ToggleButton,这个项目会被选中,而且Expander会被打开,两个是同时发生的。
最后,我们更新一下DesignerItem的模板,添加一个简单的Trigger,使得缩放的修饰框只在被选择是才可见。
<Style TargetType="{x:Type s:DesignerItem}"> <Setter Property="MinHeight" Value="50" /> <Setter Property="MinWidth" Value="50" /> <Setter Property="SnapsToDevicePixels" Value="true" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type s:DesignerItem}"> <Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=.}"> <s:MoveThumb x:Name="PART_MoveThumb" Cursor="SizeAll" Template="{StaticResource MoveThumbTemplate}" /> <ContentPresenter x:Name="PART_ContentPresenter" Content="{TemplateBinding ContentControl.Content}" Margin="{TemplateBinding Padding}" /> <s:ResizeDecorator x:Name="PART_DesignerItemDecorator" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter TargetName="PART_DesignerItemDecorator" Property="ShowDecorator" Value="True" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
Toolbox
Toolbox是一种使用ToolboxItem作为默认容器来显示控件的ItemsControl。为此,我们必须重写GetContainerForItemOverride方法和IsItemItsWenContainerOverride方法:
public class Toolbox : ItemsControl { private Size defaultItemSize = new Size(65, 65); public Size DefaultItemSize { get { return this.defaultItemSize; } set { this.defaultItemSize = value; } } protected override DependencyObject GetContainerForItemOverride() { return new ToolboxItem(); } protected override bool IsItemItsOwnContainerOverride(object item) { return (item is ToolboxItem); } }
而且,我们希望Toolbox能够使用WrapPanel作为布局面板。
<Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <WrapPanel Margin="0,5,0,5" ItemHeight="{Binding Path=DefaultItemSize.Height, RelativeSource={RelativeSource AncestorType=s:Toolbox}}" ItemWidth="{Binding Path=DefaultItemSize.Width, RelativeSource={RelativeSource AncestorType=s:Toolbox}}" /> </ItemsPanelTemplate> </Setter.Value> </Setter>
请注意,WrapPanel的ItemHeight和ItemWidth属性是与Toolbox的DefaultItemSize绑定到一起的。
ToolboxItem
如果你希望从Toolbox中拖动一个元素到Canvas中放开的话,ToolboxItem是拖动操作真正开始的地方。拖动和释放本身没有什么问题,但是你需要注意怎么把一个元素从他拖动的起点(Toolbox)复制到释放的位置(DesignerCanvas)。我们使用XamlWriter.Save方法来把ToolboxItem中的元素串行化成XAML,这种串行化有一些已知的限制,在下一节中,我们将使用二进制串行化来代替它。
public class ToolboxItem : ContentControl { private Point? dragStartPoint = null; static ToolboxItem() { FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(typeof(ToolboxItem), new FrameworkPropertyMetadata(typeof(ToolboxItem))); } protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { base.OnPreviewMouseDown(e); this.dragStartPoint = new Point?(e.GetPosition(this)); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (e.LeftButton != MouseButtonState.Pressed) { this.dragStartPoint = null; } if (this.dragStartPoint.HasValue) { Point position = e.GetPosition(this); if ((SystemParameters.MinimumHorizontalDragDistance <= Math.Abs((double)(position.X - this.dragStartPoint.Value.X))) || (SystemParameters.MinimumVerticalDragDistance <= Math.Abs((double)(position.Y - this.dragStartPoint.Value.Y)))) { string xamlString = XamlWriter.Save(this.Content); DataObject dataObject = new DataObject("DESIGNER_ITEM", xamlString); if (dataObject != null) { DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Copy); } } e.Handled = true; } } }
框选
当用户直接从DesignerCanvas上开始一个拖动动作时,一个RubberbandAdorner对象会被创建:
public class DesignerCanvas : Canvas { ... protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (e.LeftButton != MouseButtonState.Pressed) this.dragStartPoint = null; if (this.dragStartPoint.HasValue) { AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this); if (adornerLayer != null) { RubberbandAdorner adorner = new RubberbandAdorner(this, dragStartPoint); if (adorner != null) { adornerLayer.Add(adorner); } } e.Handled = true; } } ... }
一旦RubberbandAdorner被创建,他就开始接管拖动事件、绘制框选的外框、绘制被选中的元素的外框。这些更新在UpdateRubberband()和UpdateSelection()方法内部进行:
public class RubberbandAdorner : Adorner { .... private Point? startPoint, endPoint; protected override void OnMouseMove(MouseEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) { if (!this.IsMouseCaptured) { this.CaptureMouse(); } this.endPoint = e.GetPosition(this); this.UpdateRubberband(); this.UpdateSelection(); e.Handled = true; } } ... }
框选的外框实际上是一个Rectangle元素,所以UpdateRubberband()方法只需要更新他的尺寸和位置即可。
private void UpdateRubberband() { double left = Math.Min(this.startPoint.Value.X, this.endPoint.Value.X); double top = Math.Min(this.startPoint.Value.Y, this.endPoint.Value.Y); double width = Math.Abs(this.startPoint.Value.X - this.endPoint.Value.X); double height = Math.Abs(this.startPoint.Value.Y - this.endPoint.Value.Y); this.rubberband.Width = width; this.rubberband.Height = height; Canvas.SetLeft(this.rubberband, left); Canvas.SetTop(this.rubberband, top); }
在UpdateSelection()方法中还有一些地方需要处理。我们需要在这儿检查每一个DesignerItem来确定它是否在框选的范围之内。为此,VisualTreeHelper.GetDescendantBounds(item)方法提供了我们每个子级对象的外框范围。我们通过rubberband.Containes(itemBounds)来确定这些元素是否需要被选中。
private void UpdateSelection() { Rect rubberBand = new Rect(this.startPoint.Value, this.endPoint.Value); foreach (DesignerItem item in this.designerCanvas.Children) { Rect itemRect = VisualTreeHelper.GetDescendantBounds(item); Rect itemBounds = item.TransformToAncestor (designerCanvas).TransformBounds(itemRect); if (rubberBand.Contains(itemBounds)) { item.IsSelected = true; } else { item.IsSelected = false; } } }
需要注意的是,在拖动中,无论何时,如果MouseMove事件被触发都会导致上面所说的界面更新方法。它被触发的及其频繁,你也可以换一种方法:在拖动结束时(即MouseUp事件触发是)再判断这些。
自定义DragThumb
DragThumb的默认样式是一个透明的Rectangle,但是如果我们希望调整这个样式,我们可是使用一个叫做DesignerItem.DragThumbTemplate的Attached Property。下面这个示例将解释这种操作,如果DesignerItem的Content是下面这个五角星的话:
<Path Stroke="Red" StrokeThickness="5" Stretch="Fill" IsHitTestVisible="false" Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z" />
为了更好地说明,我使用了彩色的DragThumb的模板:
现在添加下面的代码:
<Path Stroke="Red" StrokeThickness="5" Stretch="Fill" IsHitTestVisible="false" Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z"> <s:DesignerItem.DragThumbTemplate> <ControlTemplate> <Path Data="M 9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7 Z" Fill="Transparent" Stretch="Fill" /> </ControlTemplate> </s:DesignerItem.DragThumbTemplate> </Path>
现在,DragThumb的样子比刚才合适多了。
(全文完)
以下为广告部分
您部署的HTTPS网站安全吗?
如果您想看下您的网站HTTPS部署的是否安全,花1分钟时间来 myssl.com 检测以下吧。让您的HTTPS网站变得更安全!
快速了解HTTPS网站安全情况。
安全评级(A+、A、A-...)、行业合规检测、证书信息查看、证书链信息以及补完、服务器套件信息、证书兼容性检测等。
安装部署SSL证书变得更方便。
SSL证书内容查看、SSL证书格式转换、CSR在线生成、SSL私钥加解密、CAA检测等。
让服务器远离SSL证书漏洞侵扰
TLS ROBOT漏洞检测、心血漏洞检测、FREAK Attack漏洞检测、SSL Poodle漏洞检测、CCS注入漏洞检测。