CanvasRenderContext.cs 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161
  1. // --------------------------------------------------------------------------------------------------------------------
  2. // <copyright file="CanvasRenderContext.cs" company="OxyPlot">
  3. // Copyright (c) 2014 OxyPlot contributors
  4. // </copyright>
  5. // <summary>
  6. // Implements <see cref="IRenderContext" /> for <see cref="System.Windows.Controls.Canvas" />.
  7. // </summary>
  8. // --------------------------------------------------------------------------------------------------------------------
  9. namespace OxyPlot.Avalonia
  10. {
  11. using global::Avalonia;
  12. using global::Avalonia.Controls;
  13. using global::Avalonia.Controls.Shapes;
  14. using global::Avalonia.Media;
  15. using global::Avalonia.Media.Imaging;
  16. using System;
  17. using System.Collections.Generic;
  18. using System.IO;
  19. using System.Linq;
  20. using Path = global::Avalonia.Controls.Shapes.Path;
  21. /// <summary>
  22. /// Implements <see cref="IRenderContext" /> for <see cref="Windows.Controls.Canvas" />.
  23. /// </summary>
  24. public class CanvasRenderContext : ClippingRenderContext
  25. {
  26. /// <summary>
  27. /// The maximum number of figures per geometry.
  28. /// </summary>
  29. private const int MaxFiguresPerGeometry = 16;
  30. /// <summary>
  31. /// The maximum number of polylines per line.
  32. /// </summary>
  33. private const int MaxPolylinesPerLine = 64;
  34. /// <summary>
  35. /// The minimum number of points per polyline.
  36. /// </summary>
  37. private const int MinPointsPerPolyline = 16;
  38. /// <summary>
  39. /// The images in use
  40. /// </summary>
  41. private readonly HashSet<OxyImage> imagesInUse = new HashSet<OxyImage>();
  42. /// <summary>
  43. /// The image cache
  44. /// </summary>
  45. private readonly Dictionary<OxyImage, Bitmap> imageCache = new Dictionary<OxyImage, Bitmap>();
  46. /// <summary>
  47. /// The brush cache.
  48. /// </summary>
  49. private readonly Dictionary<OxyColor, IBrush> brushCache = new Dictionary<OxyColor, IBrush>();
  50. /// <summary>
  51. /// The canvas.
  52. /// </summary>
  53. private readonly Canvas canvas;
  54. /// <summary>
  55. /// The clip rectangle.
  56. /// </summary>
  57. private Rect? clip;
  58. /// <summary>
  59. /// The current tool tip
  60. /// </summary>
  61. private string currentToolTip;
  62. /// <summary>
  63. /// Initializes a new instance of the <see cref="CanvasRenderContext" /> class.
  64. /// </summary>
  65. /// <param name="canvas">The canvas.</param>
  66. public CanvasRenderContext(Canvas canvas)
  67. {
  68. this.canvas = canvas;
  69. UseStreamGeometry = true;
  70. RendersToScreen = true;
  71. BalancedLineDrawingThicknessLimit = 3.5;
  72. }
  73. /// <summary>
  74. /// Gets or sets the thickness limit for "balanced" line drawing.
  75. /// </summary>
  76. public double BalancedLineDrawingThicknessLimit { get; set; }
  77. /// <summary>
  78. /// Gets or sets a value indicating whether to use stream geometry for lines and polygons rendering.
  79. /// </summary>
  80. /// <value><c>true</c> if stream geometry should be used; otherwise, <c>false</c> .</value>
  81. /// <remarks>The XamlWriter does not serialize StreamGeometry, so set this to <c>false</c> if you want to export to XAML. Using stream geometry seems to be slightly faster than using path geometry.</remarks>
  82. public bool UseStreamGeometry { get; set; }
  83. #region DrawEllipse(s)
  84. ///<inheritdoc/>
  85. public override void DrawEllipse(OxyRect rect,
  86. OxyColor fill,
  87. OxyColor stroke,
  88. double thickness,
  89. EdgeRenderingMode edgeRenderingMode)
  90. {
  91. var path = CreateAndAdd<Path>();
  92. SetStroke(path, stroke, thickness, edgeRenderingMode);
  93. if (!fill.IsUndefined())
  94. {
  95. path.Fill = GetCachedBrush(fill);
  96. }
  97. path.Data = new EllipseGeometry(ToRect(rect));
  98. }
  99. private void DrawEllipsesByStreamGeometry(
  100. IList<OxyRect> rects,
  101. OxyColor fill,
  102. OxyColor stroke,
  103. double thickness,
  104. EdgeRenderingMode edgeRenderingMode,
  105. double[] dashArray,
  106. LineJoin lineJoin)
  107. {
  108. const double ratio = 0.55228475; // (Math.Sqrt(2) - 1.0) * 4.0 / 3.0;
  109. Path path = null;
  110. StreamGeometry streamGeometry = null;
  111. StreamGeometryContext sgc = null;
  112. var count = 0;
  113. bool isSolid = !fill.IsUndefined();
  114. IBrush cachedBrush = null;
  115. if (isSolid)
  116. {
  117. cachedBrush = GetCachedBrush(fill);
  118. }
  119. foreach (var rect in rects)
  120. {
  121. if (path == null)
  122. {
  123. path = CreateAndAdd<Path>();
  124. SetStroke(path, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);
  125. if (isSolid)
  126. {
  127. path.Fill = cachedBrush;
  128. }
  129. streamGeometry = new StreamGeometry();
  130. sgc = streamGeometry.Open();
  131. sgc.SetFillRule(FillRule.NonZero);
  132. }
  133. var a = rect.Width / 2.0;
  134. var b = rect.Height / 2.0;
  135. var x0 = rect.Center.X - a;
  136. var x1 = rect.Center.X - a * ratio;
  137. var x2 = rect.Center.X;
  138. var x3 = rect.Center.X + a * ratio;
  139. var x4 = rect.Center.X + a;
  140. var y0 = rect.Center.Y - b;
  141. var y1 = rect.Center.Y - b * ratio;
  142. var y2 = rect.Center.Y;
  143. var y3 = rect.Center.Y + b * ratio;
  144. var y4 = rect.Center.Y + b;
  145. sgc.BeginFigure(new Point(x2, y0), isSolid);
  146. sgc.CubicBezierTo(new Point(x3, y0), new Point(x4, y1), new Point(x4, y2));
  147. sgc.CubicBezierTo(new Point(x4, y3), new Point(x3, y4), new Point(x2, y4));
  148. sgc.CubicBezierTo(new Point(x1, y4), new Point(x0, y3), new Point(x0, y2));
  149. sgc.CubicBezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0));
  150. sgc.EndFigure(true);
  151. count++;
  152. // Must limit the number of figures, otherwise drawing errors...
  153. if (count > MaxFiguresPerGeometry)
  154. {
  155. sgc.Dispose();
  156. path.Data = streamGeometry;
  157. path = null;
  158. count = 0;
  159. }
  160. }
  161. if (path != null)
  162. {
  163. sgc.Dispose();
  164. path.Data = streamGeometry;
  165. }
  166. }
  167. ///<inheritdoc/>
  168. public override void DrawEllipses(IList<OxyRect> rectangles,
  169. OxyColor fill,
  170. OxyColor stroke,
  171. double thickness,
  172. EdgeRenderingMode edgeRenderingMode)
  173. {
  174. if (UseStreamGeometry)
  175. {
  176. DrawEllipsesByStreamGeometry(rectangles, fill, stroke, thickness, edgeRenderingMode, null, LineJoin.Miter);
  177. return;
  178. }
  179. foreach (var rect in rectangles)
  180. {
  181. DrawEllipse(rect, fill, stroke, thickness, edgeRenderingMode);
  182. }
  183. }
  184. #endregion
  185. ///<inheritdoc/>
  186. public override void DrawLine(
  187. IList<ScreenPoint> points,
  188. OxyColor stroke,
  189. double thickness,
  190. EdgeRenderingMode edgeRenderingMode,
  191. double[] dashArray,
  192. LineJoin lineJoin)
  193. {
  194. if (thickness < BalancedLineDrawingThicknessLimit)
  195. {
  196. DrawLineBalanced(points, stroke, thickness, edgeRenderingMode, dashArray, lineJoin);
  197. return;
  198. }
  199. var e = CreateAndAdd<Polyline>();
  200. SetStroke(e, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);
  201. bool aliased = !ShouldUseAntiAliasingForLine(edgeRenderingMode, points);
  202. e.Points = ToPointCollection(points, aliased);
  203. }
  204. #region DrawLineSegments
  205. /// <summary>
  206. /// Draws the line segments by stream geometry.
  207. /// </summary>
  208. /// <param name="points">The points.</param>
  209. /// <param name="stroke">The stroke color.</param>
  210. /// <param name="thickness">The thickness.</param>
  211. /// <param name="edgeRenderingMode">The edge rendering mode.</param>
  212. /// <param name="dashArray">The dash array. Use <c>null</c> to get a solid line.</param>
  213. /// <param name="lineJoin">The line join.</param>
  214. private void DrawLineSegmentsByStreamGeometry(
  215. IList<ScreenPoint> points,
  216. OxyColor stroke,
  217. double thickness,
  218. EdgeRenderingMode edgeRenderingMode,
  219. double[] dashArray,
  220. LineJoin lineJoin)
  221. {
  222. bool aliased = edgeRenderingMode == EdgeRenderingMode.PreferSpeed;
  223. StreamGeometry streamGeometry = null;
  224. StreamGeometryContext streamGeometryContext = null;
  225. var count = 0;
  226. for (int i = 0; i + 1 < points.Count; i += 2)
  227. {
  228. if (streamGeometry == null)
  229. {
  230. streamGeometry = new StreamGeometry();
  231. streamGeometryContext = streamGeometry.Open();
  232. }
  233. streamGeometryContext.BeginFigure(ToPoint(points[i], aliased), false);
  234. streamGeometryContext.LineTo(ToPoint(points[i + 1], aliased));
  235. count++;
  236. // Must limit the number of figures, otherwise drawing errors...
  237. if (count > MaxFiguresPerGeometry || dashArray != null)
  238. {
  239. streamGeometryContext.Dispose();
  240. var path = CreateAndAdd<Path>();
  241. SetStroke(path, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);
  242. path.Data = streamGeometry;
  243. streamGeometry = null;
  244. count = 0;
  245. }
  246. }
  247. if (streamGeometry != null)
  248. {
  249. streamGeometryContext.Dispose();
  250. var path = CreateAndAdd<Path>();
  251. SetStroke(path, stroke, thickness, edgeRenderingMode, lineJoin, null, 0);
  252. path.Data = streamGeometry;
  253. }
  254. }
  255. /// <summary>
  256. /// Draws line segments defined by points (0,1) (2,3) (4,5) etc.
  257. /// This should have better performance than calling DrawLine for each segment.
  258. /// </summary>
  259. /// <param name="points">The points.</param>
  260. /// <param name="stroke">The stroke color.</param>
  261. /// <param name="thickness">The stroke thickness (in device independent units, 1/96 inch).</param>
  262. /// <param name="edgeRenderingMode">The edge rendering mode.</param>
  263. /// <param name="dashArray">The dash array (in device independent units, 1/96 inch).</param>
  264. /// <param name="lineJoin">The line join type.</param>
  265. public override void DrawLineSegments(
  266. IList<ScreenPoint> points,
  267. OxyColor stroke,
  268. double thickness,
  269. EdgeRenderingMode edgeRenderingMode,
  270. double[] dashArray,
  271. LineJoin lineJoin)
  272. {
  273. if (UseStreamGeometry)
  274. {
  275. DrawLineSegmentsByStreamGeometry(points, stroke, thickness, edgeRenderingMode, dashArray, lineJoin);
  276. return;
  277. }
  278. bool aliased = edgeRenderingMode == EdgeRenderingMode.PreferSpeed;
  279. Path path = null;
  280. PathGeometry pathGeometry = null;
  281. var count = 0;
  282. for (int i = 0; i + 1 < points.Count; i += 2)
  283. {
  284. if (path == null)
  285. {
  286. path = CreateAndAdd<Path>();
  287. SetStroke(path, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);
  288. pathGeometry = new PathGeometry();
  289. }
  290. var figure = new PathFigure { StartPoint = ToPoint(points[i], aliased), IsClosed = false };
  291. figure.Segments.Add(new LineSegment { Point = ToPoint(points[i + 1], aliased) });
  292. pathGeometry.Figures.Add(figure);
  293. count++;
  294. // Must limit the number of figures, otherwise drawing errors...
  295. if (count > MaxFiguresPerGeometry || dashArray != null)
  296. {
  297. path.Data = pathGeometry;
  298. path = null;
  299. count = 0;
  300. }
  301. }
  302. if (path != null)
  303. {
  304. path.Data = pathGeometry;
  305. }
  306. }
  307. #endregion
  308. #region DrawPolygon(s)
  309. ///<inheritdoc/>
  310. public override void DrawPolygon(
  311. IList<ScreenPoint> points,
  312. OxyColor fill,
  313. OxyColor stroke,
  314. double thickness,
  315. EdgeRenderingMode edgeRenderingMode,
  316. double[] dashArray,
  317. LineJoin lineJoin)
  318. {
  319. var e = CreateAndAdd<Polygon>();
  320. SetStroke(e, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);
  321. if (!fill.IsUndefined())
  322. {
  323. e.Fill = GetCachedBrush(fill);
  324. }
  325. bool aliased = edgeRenderingMode == EdgeRenderingMode.PreferSpeed;
  326. e.Points = ToPointCollection(points, aliased);
  327. }
  328. private void DrawPolygonsByStreamGeometry(
  329. IList<IList<ScreenPoint>> polygons,
  330. OxyColor fill,
  331. OxyColor stroke,
  332. double thickness,
  333. EdgeRenderingMode edgeRenderingMode,
  334. double[] dashArray,
  335. LineJoin lineJoin)
  336. {
  337. Path path = null;
  338. StreamGeometry streamGeometry = null;
  339. StreamGeometryContext sgc = null;
  340. var count = 0;
  341. Func<ScreenPoint, Point> toPointFunc;
  342. if (edgeRenderingMode == EdgeRenderingMode.PreferSpeed) // aliased
  343. {
  344. toPointFunc = ToPixelAlignedPoint;
  345. }
  346. else
  347. {
  348. toPointFunc = ToPoint;
  349. }
  350. bool isSolid = !fill.IsUndefined();
  351. IBrush cachedBrush = null;
  352. if (isSolid)
  353. {
  354. cachedBrush = GetCachedBrush(fill);
  355. }
  356. foreach (var polygon in polygons)
  357. {
  358. if (path == null)
  359. {
  360. path = CreateAndAdd<Path>();
  361. SetStroke(path, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);
  362. if (isSolid)
  363. {
  364. path.Fill = cachedBrush;
  365. }
  366. streamGeometry = new StreamGeometry();
  367. sgc = streamGeometry.Open();
  368. sgc.SetFillRule(FillRule.NonZero);
  369. }
  370. var first = true;
  371. foreach (var p in polygon)
  372. {
  373. if (first)
  374. {
  375. sgc.BeginFigure(toPointFunc(p), isSolid);
  376. first = false;
  377. }
  378. else
  379. {
  380. sgc.LineTo(toPointFunc(p));
  381. }
  382. }
  383. sgc.EndFigure(true);
  384. count++;
  385. // Must limit the number of figures, otherwise drawing errors...
  386. if (count > MaxFiguresPerGeometry)
  387. {
  388. sgc.Dispose();
  389. path.Data = streamGeometry;
  390. path = null;
  391. count = 0;
  392. }
  393. }
  394. if (path != null)
  395. {
  396. sgc.Dispose();
  397. path.Data = streamGeometry;
  398. }
  399. }
  400. ///<inheritdoc/>
  401. public override void DrawPolygons(
  402. IList<IList<ScreenPoint>> polygons,
  403. OxyColor fill,
  404. OxyColor stroke,
  405. double thickness,
  406. EdgeRenderingMode edgeRenderingMode,
  407. double[] dashArray,
  408. LineJoin lineJoin)
  409. {
  410. if (UseStreamGeometry)
  411. {
  412. DrawPolygonsByStreamGeometry(polygons, fill, stroke, thickness, edgeRenderingMode, dashArray, lineJoin);
  413. return;
  414. }
  415. Path path = null;
  416. PathGeometry pathGeometry = null;
  417. var count = 0;
  418. Func<ScreenPoint, Point> toPointFunc;
  419. if (edgeRenderingMode == EdgeRenderingMode.PreferSpeed) // aliased
  420. {
  421. toPointFunc = ToPixelAlignedPoint;
  422. }
  423. else
  424. {
  425. toPointFunc = ToPoint;
  426. }
  427. bool isSolid = !fill.IsUndefined();
  428. IBrush cachedBrush = null;
  429. if (isSolid)
  430. {
  431. cachedBrush = GetCachedBrush(fill);
  432. }
  433. foreach (var polygon in polygons)
  434. {
  435. if (path == null)
  436. {
  437. path = CreateAndAdd<Path>();
  438. SetStroke(path, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);
  439. if (isSolid)
  440. {
  441. path.Fill = cachedBrush;
  442. }
  443. pathGeometry = new PathGeometry { FillRule = FillRule.NonZero };
  444. }
  445. PathFigure figure = null;
  446. var first = true;
  447. foreach (var p in polygon)
  448. {
  449. if (first)
  450. {
  451. figure = new PathFigure
  452. {
  453. StartPoint = toPointFunc(p),
  454. IsFilled = isSolid,
  455. IsClosed = true
  456. };
  457. pathGeometry.Figures.Add(figure);
  458. first = false;
  459. }
  460. else
  461. {
  462. figure.Segments.Add(new LineSegment { Point = toPointFunc(p) });
  463. }
  464. }
  465. count++;
  466. // Must limit the number of figures, otherwise drawing errors...
  467. if (count > MaxFiguresPerGeometry)
  468. {
  469. path.Data = pathGeometry;
  470. path = null;
  471. count = 0;
  472. }
  473. }
  474. if (path != null)
  475. {
  476. path.Data = pathGeometry;
  477. }
  478. }
  479. #endregion
  480. /// <summary>
  481. /// Draws a rectangle.
  482. /// </summary>
  483. /// <param name="rect">The rectangle.</param>
  484. /// <param name="fill">The fill color. If set to <c>OxyColors.Undefined</c>, the rectangle will not be filled.</param>
  485. /// <param name="stroke">The stroke color. If set to <c>OxyColors.Undefined</c>, the rectangle will not be stroked.</param>
  486. /// <param name="thickness">The stroke thickness (in device independent units, 1/96 inch).</param>
  487. public override void DrawRectangle(OxyRect rect,
  488. OxyColor fill,
  489. OxyColor stroke,
  490. double thickness,
  491. EdgeRenderingMode edgeRenderingMode)
  492. {
  493. var path = CreateAndAdd<Path>();
  494. SetStroke(path, stroke, thickness, edgeRenderingMode);
  495. if (!fill.IsUndefined())
  496. {
  497. path.Fill = GetCachedBrush(fill);
  498. }
  499. path.Data = new RectangleGeometry(ToRect(rect));
  500. }
  501. /// <summary>
  502. /// Draws a collection of rectangles, where all have the same stroke and fill.
  503. /// This performs better than calling DrawRectangle multiple times.
  504. /// </summary>
  505. /// <param name="rectangles">The rectangles.</param>
  506. /// <param name="fill">The fill color. If set to <c>OxyColors.Undefined</c>, the rectangles will not be filled.</param>
  507. /// <param name="stroke">The stroke color. If set to <c>OxyColors.Undefined</c>, the rectangles will not be stroked.</param>
  508. /// <param name="thickness">The stroke thickness (in device independent units, 1/96 inch).</param>
  509. public override void DrawRectangles(IList<OxyRect> rectangles,
  510. OxyColor fill,
  511. OxyColor stroke,
  512. double thickness,
  513. EdgeRenderingMode edgeRenderingMode)
  514. {
  515. var polys = rectangles.Select(r => RectangleToPolygon(r)).ToList();
  516. DrawPolygons(polys, fill, stroke, thickness, edgeRenderingMode, null, LineJoin.Miter);
  517. }
  518. /// <summary>
  519. /// Draws text.
  520. /// </summary>
  521. /// <param name="p">The position.</param>
  522. /// <param name="text">The text.</param>
  523. /// <param name="fill">The text color.</param>
  524. /// <param name="fontFamily">The font family.</param>
  525. /// <param name="fontSize">Size of the font (in device independent units, 1/96 inch).</param>
  526. /// <param name="fontWeight">The font weight.</param>
  527. /// <param name="rotate">The rotation angle.</param>
  528. /// <param name="halign">The horizontal alignment.</param>
  529. /// <param name="valign">The vertical alignment.</param>
  530. /// <param name="maxSize">The maximum size of the text (in device independent units, 1/96 inch).</param>
  531. public override void DrawText(
  532. ScreenPoint p,
  533. string text,
  534. OxyColor fill,
  535. string fontFamily,
  536. double fontSize,
  537. double fontWeight,
  538. double rotate,
  539. HorizontalAlignment halign,
  540. VerticalAlignment valign,
  541. OxySize? maxSize)
  542. {
  543. var tb = CreateAndAdd<TextBlock>();
  544. tb.Text = text;
  545. tb.Foreground = GetCachedBrush(fill);
  546. if (fontFamily != null)
  547. {
  548. tb.FontFamily = fontFamily;
  549. }
  550. if (fontSize > 0)
  551. {
  552. tb.FontSize = fontSize;
  553. }
  554. if (fontWeight > 0)
  555. {
  556. tb.FontWeight = GetFontWeight(fontWeight);
  557. }
  558. double dx = 0;
  559. double dy = 0;
  560. if (maxSize != null || halign != HorizontalAlignment.Left || valign != VerticalAlignment.Top)
  561. {
  562. tb.Measure(new Size(1000, 1000));
  563. var size = tb.DesiredSize;
  564. if (maxSize != null)
  565. {
  566. if (size.Width > maxSize.Value.Width + 1e-3)
  567. {
  568. size = size.WithWidth(Math.Max(maxSize.Value.Width, 0));
  569. }
  570. if (size.Height > maxSize.Value.Height + 1e-3)
  571. {
  572. size = size.WithHeight(Math.Max(maxSize.Value.Height, 0));
  573. }
  574. tb.Width = size.Width;
  575. tb.Height = size.Height;
  576. }
  577. if (halign == HorizontalAlignment.Center)
  578. {
  579. dx = -size.Width / 2;
  580. }
  581. if (halign == HorizontalAlignment.Right)
  582. {
  583. dx = -size.Width;
  584. }
  585. if (valign == VerticalAlignment.Middle)
  586. {
  587. dy = -size.Height / 2;
  588. }
  589. if (valign == VerticalAlignment.Bottom)
  590. {
  591. dy = -size.Height;
  592. }
  593. }
  594. var transform = new TransformGroup();
  595. transform.Children.Add(new TranslateTransform(dx, dy));
  596. if (Math.Abs(rotate) > double.Epsilon)
  597. {
  598. transform.Children.Add(new RotateTransform(rotate));
  599. }
  600. transform.Children.Add(new TranslateTransform(p.X, p.Y));
  601. tb.RenderTransform = transform;
  602. if (tb.Clip != null)
  603. {
  604. tb.Clip.Transform = new MatrixTransform(tb.RenderTransform.Value.Invert());
  605. }
  606. tb.RenderTransformOrigin = new RelativePoint(0.0, 0.0, RelativeUnit.Relative);
  607. }
  608. /// <summary>
  609. /// Measures the size of the specified text.
  610. /// </summary>
  611. /// <param name="text">The text.</param>
  612. /// <param name="fontFamily">The font family.</param>
  613. /// <param name="fontSize">Size of the font (in device independent units, 1/96 inch).</param>
  614. /// <param name="fontWeight">The font weight.</param>
  615. /// <returns>
  616. /// The size of the text (in device independent units, 1/96 inch).
  617. /// </returns>
  618. public override OxySize MeasureText(string text,
  619. string fontFamily,
  620. double fontSize,
  621. double fontWeight)
  622. {
  623. if (string.IsNullOrEmpty(text))
  624. {
  625. return OxySize.Empty;
  626. }
  627. var tb = new TextBlock { Text = text };
  628. if (fontFamily != null)
  629. {
  630. tb.FontFamily = fontFamily;
  631. }
  632. if (fontSize > 0)
  633. {
  634. tb.FontSize = fontSize;
  635. }
  636. if (fontWeight > 0)
  637. {
  638. tb.FontWeight = GetFontWeight(fontWeight);
  639. }
  640. tb.Measure(new Size(1000, 1000));
  641. return new OxySize(tb.DesiredSize.Width, tb.DesiredSize.Height);
  642. }
  643. /// <summary>
  644. /// Sets the tool tip for the following items.
  645. /// </summary>
  646. /// <param name="text">The text in the tool tip.</param>
  647. public override void SetToolTip(string text)
  648. {
  649. currentToolTip = text;
  650. }
  651. /// <summary>
  652. /// Draws a portion of the specified <see cref="OxyImage" />.
  653. /// </summary>
  654. /// <param name="source">The source.</param>
  655. /// <param name="srcX">The x-coordinate of the upper-left corner of the portion of the source image to draw.</param>
  656. /// <param name="srcY">The y-coordinate of the upper-left corner of the portion of the source image to draw.</param>
  657. /// <param name="srcWidth">Width of the portion of the source image to draw.</param>
  658. /// <param name="srcHeight">Height of the portion of the source image to draw.</param>
  659. /// <param name="destX">The x-coordinate of the upper-left corner of drawn image.</param>
  660. /// <param name="destY">The y-coordinate of the upper-left corner of drawn image.</param>
  661. /// <param name="destWidth">The width of the drawn image.</param>
  662. /// <param name="destHeight">The height of the drawn image.</param>
  663. /// <param name="opacity">The opacity.</param>
  664. /// <param name="interpolate">interpolate if set to <c>true</c>.</param>
  665. public override void DrawImage(
  666. OxyImage source,
  667. double srcX,
  668. double srcY,
  669. double srcWidth,
  670. double srcHeight,
  671. double destX,
  672. double destY,
  673. double destWidth,
  674. double destHeight,
  675. double opacity,
  676. bool interpolate)
  677. {
  678. if (destWidth <= 0 || destHeight <= 0 || srcWidth <= 0 || srcHeight <= 0)
  679. {
  680. return;
  681. }
  682. var image = CreateAndAdd<Image>(destX, destY);
  683. var bitmapChain = GetImageSource(source);
  684. // ReSharper disable CompareOfFloatsByEqualityOperator
  685. if (srcX == 0 && srcY == 0 && srcWidth == bitmapChain.PixelSize.Width && srcHeight == bitmapChain.PixelSize.Height)
  686. // ReSharper restore CompareOfFloatsByEqualityOperator
  687. {
  688. // do not crop
  689. }
  690. else
  691. {
  692. image.Clip = new RectangleGeometry(new Rect(srcX, srcY, srcWidth, srcHeight));
  693. }
  694. image.Opacity = opacity;
  695. image.Width = destWidth;
  696. image.Height = destHeight;
  697. image.Stretch = Stretch.Fill;
  698. // Set the position of the image
  699. Canvas.SetLeft(image, destX);
  700. Canvas.SetTop(image, destY);
  701. //// alternative: image.RenderTransform = new TranslateTransform(destX, destY);
  702. image.Source = bitmapChain;
  703. if (interpolate)
  704. {
  705. RenderOptions.SetBitmapInterpolationMode(image, BitmapInterpolationMode.LowQuality);
  706. }
  707. else
  708. {
  709. RenderOptions.SetBitmapInterpolationMode(image, BitmapInterpolationMode.None);
  710. }
  711. }
  712. /// <summary>
  713. /// Sets the clipping rectangle.
  714. /// </summary>
  715. /// <param name="clippingRect">The clipping rectangle.</param>
  716. /// <returns><c>true</c> if the clip rectangle was set.</returns>
  717. protected override void SetClip(OxyRect clippingRect)
  718. {
  719. clip = ToRect(clippingRect);
  720. }
  721. /// <summary>
  722. /// Resets the clip rectangle.
  723. /// </summary>
  724. protected override void ResetClip()
  725. {
  726. clip = null;
  727. }
  728. /// <summary>
  729. /// Cleans up resources not in use.
  730. /// </summary>
  731. /// <remarks>This method is called at the end of each rendering.</remarks>
  732. public override void CleanUp()
  733. {
  734. // Find the images in the cache that has not been used since last call to this method
  735. // Remove the images from the cache
  736. foreach (var i in imageCache.Keys.Where(i => !imagesInUse.Contains(i)).ToList())
  737. {
  738. imageCache.Remove(i);
  739. }
  740. imagesInUse.Clear();
  741. }
  742. /// <summary>
  743. /// Gets the font weight.
  744. /// </summary>
  745. /// <param name="fontWeight">The font weight value.</param>
  746. /// <returns>The font weight.</returns>
  747. private static FontWeight GetFontWeight(double fontWeight)
  748. {
  749. return fontWeight > FontWeights.Normal ? FontWeight.Bold : FontWeight.Normal;
  750. }
  751. /// <summary>
  752. /// Creates an element of the specified type and adds it to the canvas.
  753. /// </summary>
  754. /// <typeparam name="T">Type of element to create.</typeparam>
  755. /// <param name="clipOffsetX">The clip offset executable.</param>
  756. /// <param name="clipOffsetY">The clip offset asynchronous.</param>
  757. /// <returns>The element.</returns>
  758. private T CreateAndAdd<T>(double clipOffsetX = 0, double clipOffsetY = 0) where T : Control, new()
  759. {
  760. // TODO: here we can reuse existing elements in the canvas.Children collection
  761. var element = new T();
  762. if (clip != null)
  763. {
  764. element.Clip = new RectangleGeometry(
  765. new Rect(
  766. clip.Value.X - clipOffsetX,
  767. clip.Value.Y - clipOffsetY,
  768. clip.Value.Width,
  769. clip.Value.Height));
  770. }
  771. canvas.Children.Add(element);
  772. ApplyToolTip(element);
  773. return element;
  774. }
  775. /// <summary>
  776. /// Applies the current tool tip to the specified element.
  777. /// </summary>
  778. /// <param name="element">The element.</param>
  779. private void ApplyToolTip(Control element)
  780. {
  781. if (!string.IsNullOrEmpty(currentToolTip))
  782. {
  783. ToolTip.SetTip(element, currentToolTip);
  784. }
  785. }
  786. /// <summary>
  787. /// Gets the cached brush.
  788. /// </summary>
  789. /// <param name="color">The color.</param>
  790. /// <returns>The brush.</returns>
  791. private IBrush GetCachedBrush(OxyColor color)
  792. {
  793. if (color.A == 0)
  794. {
  795. return null;
  796. }
  797. if (!brushCache.TryGetValue(color, out IBrush brush))
  798. {
  799. brush = new SolidColorBrush(color.ToColor());
  800. brushCache.Add(color, brush);
  801. }
  802. return brush;
  803. }
  804. /// <summary>
  805. /// Sets the stroke properties of the specified shape object.
  806. /// </summary>
  807. /// <param name="shape">The shape.</param>
  808. /// <param name="stroke">The stroke color.</param>
  809. /// <param name="thickness">The thickness.</param>
  810. /// <param name="edgeRenderingMode">The edge rendering mode.</param>
  811. /// <param name="lineJoin">The line join.</param>
  812. /// <param name="dashArray">The dash array. Use <c>null</c> to get a solid line.</param>
  813. /// <param name="dashOffset">The dash offset.</param>
  814. private void SetStroke(
  815. Shape shape,
  816. OxyColor stroke,
  817. double thickness,
  818. EdgeRenderingMode edgeRenderingMode,
  819. LineJoin lineJoin = LineJoin.Miter,
  820. IEnumerable<double> dashArray = null,
  821. double dashOffset = 0)
  822. {
  823. if (thickness > 0 && !stroke.IsUndefined())
  824. {
  825. shape.Stroke = GetCachedBrush(stroke);
  826. switch (lineJoin)
  827. {
  828. case LineJoin.Round:
  829. shape.StrokeJoin = PenLineJoin.Round;
  830. break;
  831. case LineJoin.Bevel:
  832. shape.StrokeJoin = PenLineJoin.Bevel;
  833. break;
  834. // The default StrokeLineJoin is Miter
  835. }
  836. if (thickness > 0)
  837. {
  838. shape.StrokeThickness = thickness;
  839. }
  840. if (dashArray != null) // what happens if thickness is 0?
  841. {
  842. // ?????? shape.StrokeDashArray = new global::Avalonia.Collections.AvaloniaList<double>(dashArray.Select(dash => dash));
  843. shape.StrokeDashArray = new global::Avalonia.Collections.AvaloniaList<double>(dashArray);
  844. shape.StrokeDashOffset = dashOffset;
  845. }
  846. }
  847. if (edgeRenderingMode == EdgeRenderingMode.PreferSpeed)
  848. {
  849. // TODO: use alised lines
  850. }
  851. }
  852. /// <summary>
  853. /// Gets the bitmap source.
  854. /// </summary>
  855. /// <param name="image">The image.</param>
  856. /// <returns>The bitmap source.</returns>
  857. private Bitmap GetImageSource(OxyImage image)
  858. {
  859. if (image == null)
  860. {
  861. return null;
  862. }
  863. if (!imagesInUse.Contains(image))
  864. {
  865. imagesInUse.Add(image);
  866. }
  867. if (imageCache.TryGetValue(image, out var src))
  868. {
  869. return src;
  870. }
  871. using (var ms = new MemoryStream(image.GetData()))
  872. {
  873. var btm = new Bitmap(ms);
  874. imageCache.Add(image, btm);
  875. return btm;
  876. }
  877. }
  878. /// <summary>
  879. /// Draws the line using the MaxPolylinesPerLine and MinPointsPerPolyline properties.
  880. /// </summary>
  881. /// <param name="points">The points.</param>
  882. /// <param name="stroke">The stroke color.</param>
  883. /// <param name="thickness">The thickness.</param>
  884. /// <param name="edgeRenderingMode">The edge rendering mode.</param>
  885. /// <param name="dashArray">The dash array. Use <c>null</c> to get a solid line.</param>
  886. /// <param name="lineJoin">The line join.</param>
  887. /// <remarks>See <a href="https://oxyplot.codeplex.com/discussions/456679">discussion</a>.</remarks>
  888. private void DrawLineBalanced(IList<ScreenPoint> points, OxyColor stroke, double thickness, EdgeRenderingMode edgeRenderingMode, double[] dashArray, LineJoin lineJoin)
  889. {
  890. // balance the number of points per polyline and the number of polylines
  891. var numPointsPerPolyline = Math.Max(points.Count / MaxPolylinesPerLine, MinPointsPerPolyline);
  892. var polyline = CreateAndAdd<Polyline>();
  893. SetStroke(polyline, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, 0);
  894. var pc = new List<Point>(numPointsPerPolyline);
  895. var n = points.Count;
  896. double lineLength = 0;
  897. var dashPatternLength = (dashArray != null) ? dashArray.Sum() : 0;
  898. var last = new Point();
  899. for (int i = 0; i < n; i++)
  900. {
  901. bool aliased = !this.ShouldUseAntiAliasingForLine(edgeRenderingMode, points);
  902. var p = aliased ? ToPixelAlignedPoint(points[i]) : ToPoint(points[i]);
  903. pc.Add(p);
  904. // alt. 1
  905. if (dashArray != null)
  906. {
  907. if (i > 0)
  908. {
  909. var delta = p - last;
  910. var dist = Math.Sqrt((delta.X * delta.X) + (delta.Y * delta.Y));
  911. lineLength += dist;
  912. }
  913. last = p;
  914. }
  915. // use multiple polylines with limited number of points to improve Avalonia performance
  916. if (pc.Count >= numPointsPerPolyline)
  917. {
  918. polyline.Points = pc;
  919. if (i < n - 1)
  920. {
  921. // alt.2
  922. ////if (dashArray != null)
  923. ////{
  924. //// lineLength += this.GetLength(polyline);
  925. ////}
  926. // start a new polyline at last point so there is no gap (it is not necessary to use the % operator)
  927. var dashOffset = dashPatternLength > 0 ? lineLength / thickness : 0;
  928. polyline = CreateAndAdd<Polyline>();
  929. SetStroke(polyline, stroke, thickness, edgeRenderingMode, lineJoin, dashArray, dashOffset);
  930. pc = new List<Point>(numPointsPerPolyline) { pc.Last() };
  931. }
  932. }
  933. }
  934. if (pc.Count > 1 || n == 1)
  935. {
  936. polyline.Points = pc;
  937. }
  938. }
  939. /// <summary>
  940. /// Converts a <see cref="ScreenPoint" /> to a <see cref="Point" />.
  941. /// </summary>
  942. /// <param name="pt">The screen point.</param>
  943. /// <returns>A <see cref="Point" />.</returns>
  944. private static Point ToPoint(ScreenPoint pt)
  945. {
  946. return new Point(pt.X, pt.Y);
  947. }
  948. /// <summary>
  949. /// Converts a <see cref="ScreenPoint" /> to a pixel aligned<see cref="Point" />.
  950. /// </summary>
  951. /// <param name="pt">The screen point.</param>
  952. /// <returns>A pixel aligned <see cref="Point" />.</returns>
  953. private static Point ToPixelAlignedPoint(ScreenPoint pt)
  954. {
  955. // adding 0.5 to get pixel boundary alignment, seems to work
  956. // http://weblogs.asp.net/mschwarz/archive/2008/01/04/silverlight-rectangles-paths-and-line-comparison.aspx
  957. // http://www.wynapse.com/Silverlight/Tutor/Silverlight_Rectangles_Paths_And_Lines_Comparison.aspx
  958. // TODO: issue 10221 - should consider line thickness and logical to physical size of pixels
  959. return new Point(0.5 + (int)pt.X, 0.5 + (int)pt.Y);
  960. }
  961. /// <summary>
  962. /// Converts an <see cref="OxyRect" /> to a <see cref="Rect" />.
  963. /// </summary>
  964. /// <param name="r">The rectangle.</param>
  965. /// <returns>A <see cref="Rect" />.</returns>
  966. private static Rect ToRect(OxyRect r)
  967. {
  968. return new Rect(r.Left, r.Top, r.Width, r.Height);
  969. }
  970. /// <summary>
  971. /// Converts an <see cref="OxyRect" /> to a pixel aligned <see cref="Rect" />.
  972. /// </summary>
  973. /// <param name="r">The rectangle.</param>
  974. /// <returns>A pixel aligned<see cref="Rect" />.</returns>
  975. private static Rect ToPixelAlignedRect(OxyRect r)
  976. {
  977. // TODO: similar changes as in ToPixelAlignedPoint
  978. var x = 0.5 + (int)r.Left;
  979. var y = 0.5 + (int)r.Top;
  980. var ri = 0.5 + (int)r.Right;
  981. var bo = 0.5 + (int)r.Bottom;
  982. return new Rect(x, y, ri - x, bo - y);
  983. }
  984. /// <summary>
  985. /// Converts a <see cref="ScreenPoint" /> to a <see cref="Point" />.
  986. /// </summary>
  987. /// <param name="pt">The screen point.</param>
  988. /// <param name="aliased">use pixel alignment conversion if set to <c>true</c>.</param>
  989. /// <returns>A <see cref="Point" />.</returns>
  990. private static Point ToPoint(ScreenPoint pt, bool aliased)
  991. {
  992. return aliased ? ToPixelAlignedPoint(pt) : ToPoint(pt);
  993. }
  994. /// <summary>
  995. /// Creates a point collection from the specified points.
  996. /// </summary>
  997. /// <param name="points">The points to convert.</param>
  998. /// <param name="aliased">convert to pixel aligned points if set to <c>true</c>.</param>
  999. /// <returns>The point collection.</returns>
  1000. private static List<Point> ToPointCollection(IEnumerable<ScreenPoint> points, bool aliased)
  1001. {
  1002. return new List<Point>(aliased ? points.Select(ToPixelAlignedPoint) : points.Select(ToPoint));
  1003. }
  1004. /// <summary>
  1005. /// Converts an <see cref="OxyRect"/> to an array of <see cref="ScreenPoint"/>.
  1006. /// </summary>
  1007. /// <param name="rect">The rectangle.</param>
  1008. /// <returns>A <see cref="ScreenPoint[]"/>.</returns>
  1009. private static IList<ScreenPoint> RectangleToPolygon(OxyRect rect)
  1010. {
  1011. return new ScreenPoint[]
  1012. {
  1013. rect.BottomLeft,
  1014. rect.TopLeft,
  1015. rect.TopRight,
  1016. rect.BottomRight,
  1017. };
  1018. }
  1019. }
  1020. }