BehaviorView.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using System.Windows.Data;
  8. using System.Windows.Documents;
  9. using System.Windows.Input;
  10. using System.Windows.Media;
  11. using System.Windows.Media.Imaging;
  12. using System.Windows.Navigation;
  13. using System.Windows.Shapes;
  14. namespace Controls.BehaviorView
  15. {
  16. /// <summary>
  17. /// 按照步骤 1a 或 1b 操作,然后执行步骤 2 以在 XAML 文件中使用此自定义控件。
  18. ///
  19. /// 步骤 1a) 在当前项目中存在的 XAML 文件中使用该自定义控件。
  20. /// 将此 XmlNamespace 特性添加到要使用该特性的标记文件的根
  21. /// 元素中:
  22. ///
  23. /// xmlns:MyNamespace="clr-namespace:BehaviorView"
  24. ///
  25. ///
  26. /// 步骤 1b) 在其他项目中存在的 XAML 文件中使用该自定义控件。
  27. /// 将此 XmlNamespace 特性添加到要使用该特性的标记文件的根
  28. /// 元素中:
  29. ///
  30. /// xmlns:MyNamespace="clr-namespace:BehaviorView;assembly=BehaviorView"
  31. ///
  32. /// 您还需要添加一个从 XAML 文件所在的项目到此项目的项目引用,
  33. /// 并重新生成以避免编译错误:
  34. ///
  35. /// 在解决方案资源管理器中右击目标项目,然后依次单击
  36. /// “添加引用”->“项目”->[选择此项目]
  37. ///
  38. ///
  39. /// 步骤 2)
  40. /// 继续操作并在 XAML 文件中使用控件。
  41. ///
  42. /// <MyNamespace:CustomControl1/>
  43. ///
  44. /// </summary>
  45. /// <summary>
  46. /// A Canvas which manages dragging of the UIElements it contains.
  47. /// </summary>
  48. public class BehaviorView : Canvas
  49. {
  50. // Stores a reference to the UIElement currently being dragged by the user.
  51. private UIElement elementBeingDragged;
  52. // Keeps track of where the mouse cursor was when a drag operation began.
  53. private Point origCursorLocation;
  54. // The offsets from the DragCanvas' edges when the drag operation began.
  55. private double origHorizOffset, origVertOffset;
  56. // Keeps track of which horizontal and vertical offset should be modified for the drag element.
  57. private bool modifyLeftOffset, modifyTopOffset;
  58. // True if a drag operation is underway, else false.
  59. private bool isDragInProgress;
  60. public static readonly DependencyProperty CanBeDraggedProperty;
  61. public static bool GetCanBeDragged(UIElement uiElement)
  62. {
  63. if (uiElement == null)
  64. return false;
  65. return (bool)uiElement.GetValue(CanBeDraggedProperty);
  66. }
  67. public static void SetCanBeDragged(UIElement uiElement, bool value)
  68. {
  69. if (uiElement != null)
  70. uiElement.SetValue(CanBeDraggedProperty, value);
  71. }
  72. public static readonly DependencyProperty AllowDraggingProperty;
  73. public static readonly DependencyProperty AllowDragOutOfViewProperty;
  74. static BehaviorView()
  75. {
  76. AllowDraggingProperty = DependencyProperty.Register(
  77. "AllowDragging",
  78. typeof(bool),
  79. typeof(BehaviorView),
  80. new PropertyMetadata(true));
  81. AllowDragOutOfViewProperty = DependencyProperty.Register(
  82. "AllowDragOutOfView",
  83. typeof(bool),
  84. typeof(BehaviorView),
  85. new UIPropertyMetadata(false));
  86. CanBeDraggedProperty = DependencyProperty.RegisterAttached(
  87. "CanBeDragged",
  88. typeof(bool),
  89. typeof(BehaviorView),
  90. new UIPropertyMetadata(true));
  91. }
  92. /// <summary>
  93. /// Initializes a new instance of DragCanvas. UIElements in
  94. /// the DragCanvas will immediately be draggable by the user.
  95. /// </summary>
  96. public BehaviorView()
  97. {
  98. }
  99. /// <summary>
  100. /// Gets/sets whether elements in the DragCanvas should be draggable by the user.
  101. /// The default value is true. This is a dependency property.
  102. /// </summary>
  103. public bool AllowDragging
  104. {
  105. get { return (bool)base.GetValue(AllowDraggingProperty); }
  106. set { base.SetValue(AllowDraggingProperty, value); }
  107. }
  108. /// <summary>
  109. /// Gets/sets whether the user should be able to drag elements in the DragCanvas out of
  110. /// the viewable area. The default value is false. This is a dependency property.
  111. /// </summary>
  112. public bool AllowDragOutOfView
  113. {
  114. get { return (bool)GetValue(AllowDragOutOfViewProperty); }
  115. set { SetValue(AllowDragOutOfViewProperty, value); }
  116. }
  117. /// <summary>
  118. /// Assigns the element a z-index which will ensure that
  119. /// it is in front of every other element in the Canvas.
  120. /// The z-index of every element whose z-index is between
  121. /// the element's old and new z-index will have its z-index
  122. /// decremented by one.
  123. /// </summary>
  124. /// <param name="targetElement">
  125. /// The element to be sent to the front of the z-order.
  126. /// </param>
  127. public void BringToFront(UIElement element)
  128. {
  129. this.UpdateZOrder(element, true);
  130. }
  131. /// <summary>
  132. /// Assigns the element a z-index which will ensure that
  133. /// it is behind every other element in the Canvas.
  134. /// The z-index of every element whose z-index is between
  135. /// the element's old and new z-index will have its z-index
  136. /// incremented by one.
  137. /// </summary>
  138. /// <param name="targetElement">
  139. /// The element to be sent to the back of the z-order.
  140. /// </param>
  141. public void SendToBack(UIElement element)
  142. {
  143. this.UpdateZOrder(element, false);
  144. }
  145. /// <summary>
  146. /// Returns the UIElement currently being dragged, or null.
  147. /// </summary>
  148. /// <remarks>
  149. /// Note to inheritors: This property exposes a protected
  150. /// setter which should be used to modify the drag element.
  151. /// </remarks>
  152. public UIElement ElementBeingDragged
  153. {
  154. get
  155. {
  156. if (!this.AllowDragging)
  157. return null;
  158. else
  159. return this.elementBeingDragged;
  160. }
  161. protected set
  162. {
  163. if (this.elementBeingDragged != null)
  164. this.elementBeingDragged.ReleaseMouseCapture();
  165. if (!this.AllowDragging)
  166. this.elementBeingDragged = null;
  167. else
  168. {
  169. if (BehaviorView.GetCanBeDragged(value))
  170. {
  171. this.elementBeingDragged = value;
  172. this.elementBeingDragged.CaptureMouse();
  173. }
  174. else
  175. this.elementBeingDragged = null;
  176. }
  177. }
  178. }
  179. /// <summary>
  180. /// Walks up the visual tree starting with the specified DependencyObject,
  181. /// looking for a UIElement which is a child of the Canvas. If a suitable
  182. /// element is not found, null is returned. If the 'depObj' object is a
  183. /// UIElement in the Canvas's Children collection, it will be returned.
  184. /// </summary>
  185. /// <param name="depObj">
  186. /// A DependencyObject from which the search begins.
  187. /// </param>
  188. public UIElement FindCanvasChild(DependencyObject depObj)
  189. {
  190. while (depObj != null)
  191. {
  192. // If the current object is a UIElement which is a child of the
  193. // Canvas, exit the loop and return it.
  194. UIElement elem = depObj as UIElement;
  195. if (elem != null && base.Children.Contains(elem))
  196. break;
  197. // VisualTreeHelper works with objects of type Visual or Visual3D.
  198. // If the current object is not derived from Visual or Visual3D,
  199. // then use the LogicalTreeHelper to find the parent element.
  200. if (depObj is Visual || depObj is Visual3D)
  201. depObj = VisualTreeHelper.GetParent(depObj);
  202. else
  203. depObj = LogicalTreeHelper.GetParent(depObj);
  204. }
  205. return depObj as UIElement;
  206. }
  207. protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
  208. {
  209. base.OnPreviewMouseLeftButtonDown(e);
  210. this.isDragInProgress = false;
  211. // Cache the mouse cursor location.
  212. this.origCursorLocation = e.GetPosition(this);
  213. // Walk up the visual tree from the element that was clicked,
  214. // looking for an element that is a direct child of the Canvas.
  215. this.ElementBeingDragged = this.FindCanvasChild(e.Source as DependencyObject);
  216. if (this.ElementBeingDragged == null)
  217. return;
  218. // Get the element's offsets from the four sides of the Canvas.
  219. double left = Canvas.GetLeft(this.ElementBeingDragged);
  220. double right = Canvas.GetRight(this.ElementBeingDragged);
  221. double top = Canvas.GetTop(this.ElementBeingDragged);
  222. double bottom = Canvas.GetBottom(this.ElementBeingDragged);
  223. // Calculate the offset deltas and determine for which sides
  224. // of the Canvas to adjust the offsets.
  225. this.origHorizOffset = ResolveOffset(left, right, out this.modifyLeftOffset);
  226. this.origVertOffset = ResolveOffset(top, bottom, out this.modifyTopOffset);
  227. // Set the Handled flag so that a control being dragged
  228. // does not react to the mouse input.
  229. e.Handled = true;
  230. this.isDragInProgress = true;
  231. }
  232. protected override void OnPreviewMouseMove(MouseEventArgs e)
  233. {
  234. base.OnPreviewMouseMove(e);
  235. // If no element is being dragged, there is nothing to do.
  236. if (this.ElementBeingDragged == null || !this.isDragInProgress)
  237. return;
  238. // Get the position of the mouse cursor, relative to the Canvas.
  239. Point cursorLocation = e.GetPosition(this);
  240. // These values will store the new offsets of the drag element.
  241. double newHorizontalOffset, newVerticalOffset;
  242. // Determine the horizontal offset.
  243. if (this.modifyLeftOffset)
  244. newHorizontalOffset = this.origHorizOffset + (cursorLocation.X - this.origCursorLocation.X);
  245. else
  246. newHorizontalOffset = this.origHorizOffset - (cursorLocation.X - this.origCursorLocation.X);
  247. // Determine the vertical offset.
  248. if (this.modifyTopOffset)
  249. newVerticalOffset = this.origVertOffset + (cursorLocation.Y - this.origCursorLocation.Y);
  250. else
  251. newVerticalOffset = this.origVertOffset - (cursorLocation.Y - this.origCursorLocation.Y);
  252. if (!this.AllowDragOutOfView)
  253. {
  254. // Get the bounding rect of the drag element.
  255. Rect elemRect = this.CalculateDragElementRect(newHorizontalOffset, newVerticalOffset);
  256. //
  257. // If the element is being dragged out of the viewable area,
  258. // determine the ideal rect location, so that the element is
  259. // within the edge(s) of the canvas.
  260. //
  261. bool leftAlign = elemRect.Left < 0;
  262. bool rightAlign = elemRect.Right > this.ActualWidth;
  263. if (leftAlign)
  264. newHorizontalOffset = modifyLeftOffset ? 0 : this.ActualWidth - elemRect.Width;
  265. else if (rightAlign)
  266. newHorizontalOffset = modifyLeftOffset ? this.ActualWidth - elemRect.Width : 0;
  267. bool topAlign = elemRect.Top < 0;
  268. bool bottomAlign = elemRect.Bottom > this.ActualHeight;
  269. if (topAlign)
  270. newVerticalOffset = modifyTopOffset ? 0 : this.ActualHeight - elemRect.Height;
  271. else if (bottomAlign)
  272. newVerticalOffset = modifyTopOffset ? this.ActualHeight - elemRect.Height : 0;
  273. }
  274. if (this.modifyLeftOffset)
  275. Canvas.SetLeft(this.ElementBeingDragged, newHorizontalOffset);
  276. else
  277. Canvas.SetRight(this.ElementBeingDragged, newHorizontalOffset);
  278. if (this.modifyTopOffset)
  279. Canvas.SetTop(this.ElementBeingDragged, newVerticalOffset);
  280. else
  281. Canvas.SetBottom(this.ElementBeingDragged, newVerticalOffset);
  282. }
  283. protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
  284. {
  285. base.OnPreviewMouseUp(e);
  286. // Reset the field whether the left or right mouse button was
  287. // released, in case a context menu was opened on the drag element.
  288. this.ElementBeingDragged = null;
  289. }
  290. /// <summary>
  291. /// Returns a Rect which describes the bounds of the element being dragged.
  292. /// </summary>
  293. private Rect CalculateDragElementRect(double newHorizOffset, double newVertOffset)
  294. {
  295. if (this.ElementBeingDragged == null)
  296. throw new InvalidOperationException("ElementBeingDragged is null.");
  297. Size elemSize = this.ElementBeingDragged.RenderSize;
  298. double x, y;
  299. if (this.modifyLeftOffset)
  300. x = newHorizOffset;
  301. else
  302. x = this.ActualWidth - newHorizOffset - elemSize.Width;
  303. if (this.modifyTopOffset)
  304. y = newVertOffset;
  305. else
  306. y = this.ActualHeight - newVertOffset - elemSize.Height;
  307. Point elemLoc = new Point(x, y);
  308. return new Rect(elemLoc, elemSize);
  309. }
  310. /// <summary>
  311. /// Determines one component of a UIElement's location
  312. /// within a Canvas (either the horizontal or vertical offset).
  313. /// </summary>
  314. /// <param name="side1">
  315. /// The value of an offset relative to a default side of the
  316. /// Canvas (i.e. top or left).
  317. /// </param>
  318. /// <param name="side2">
  319. /// The value of the offset relative to the other side of the
  320. /// Canvas (i.e. bottom or right).
  321. /// </param>
  322. /// <param name="useSide1">
  323. /// Will be set to true if the returned value should be used
  324. /// for the offset from the side represented by the 'side1'
  325. /// parameter. Otherwise, it will be set to false.
  326. /// </param>
  327. private static double ResolveOffset(double side1, double side2, out bool useSide1)
  328. {
  329. // If the Canvas.Left and Canvas.Right attached properties
  330. // are specified for an element, the 'Left' value is honored.
  331. // The 'Top' value is honored if both Canvas.Top and
  332. // Canvas.Bottom are set on the same element. If one
  333. // of those attached properties is not set on an element,
  334. // the default value is Double.NaN.
  335. useSide1 = true;
  336. double result;
  337. if (Double.IsNaN(side1))
  338. {
  339. if (Double.IsNaN(side2))
  340. {
  341. // Both sides have no value, so set the
  342. // first side to a value of zero.
  343. result = 0;
  344. }
  345. else
  346. {
  347. result = side2;
  348. useSide1 = false;
  349. }
  350. }
  351. else
  352. {
  353. result = side1;
  354. }
  355. return result;
  356. }
  357. /// <summary>
  358. /// Helper method used by the BringToFront and SendToBack methods.
  359. /// </summary>
  360. /// <param name="element">
  361. /// The element to bring to the front or send to the back.
  362. /// </param>
  363. /// <param name="bringToFront">
  364. /// Pass true if calling from BringToFront, else false.
  365. /// </param>
  366. private void UpdateZOrder(UIElement element, bool bringToFront)
  367. {
  368. if (element == null)
  369. throw new ArgumentNullException("element");
  370. if (!base.Children.Contains(element))
  371. throw new ArgumentException("Must be a child element of the Canvas.", "element");
  372. // Determine the Z-Index for the target UIElement.
  373. int elementNewZIndex = -1;
  374. if (bringToFront)
  375. {
  376. foreach (UIElement elem in base.Children)
  377. if (elem.Visibility != Visibility.Collapsed)
  378. ++elementNewZIndex;
  379. }
  380. else
  381. {
  382. elementNewZIndex = 0;
  383. }
  384. // Determine if the other UIElements' Z-Index
  385. // should be raised or lowered by one.
  386. int offset = (elementNewZIndex == 0) ? +1 : -1;
  387. int elementCurrentZIndex = Canvas.GetZIndex(element);
  388. // Update the Z-Index of every UIElement in the Canvas.
  389. foreach (UIElement childElement in base.Children)
  390. {
  391. if (childElement == element)
  392. Canvas.SetZIndex(element, elementNewZIndex);
  393. else
  394. {
  395. int zIndex = Canvas.GetZIndex(childElement);
  396. // Only modify the z-index of an element if it is
  397. // in between the target element's old and new z-index.
  398. if (bringToFront && elementCurrentZIndex < zIndex ||
  399. !bringToFront && zIndex < elementCurrentZIndex)
  400. {
  401. Canvas.SetZIndex(childElement, zIndex + offset);
  402. }
  403. }
  404. }
  405. }
  406. }
  407. }