DrawText.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. using System;
  2. using System.Collections;
  3. using System.Drawing;
  4. using System.Drawing.Drawing2D;
  5. using System.ComponentModel;
  6. using System.Collections.Generic;
  7. using System.Drawing.Text;
  8. using System.Globalization;
  9. namespace FastReport.Utils
  10. {
  11. /// <summary>
  12. /// Used to draw a text with non-standard angle or justification.
  13. /// </summary>
  14. internal class DrawText
  15. {
  16. private List<Paragraph> paragraphs;
  17. #region Properties
  18. public List<Paragraph> Paragraphs
  19. {
  20. get { return paragraphs; }
  21. }
  22. #endregion
  23. #region Private Methods
  24. private void SplitToParagraphs(string text)
  25. {
  26. string[] lines = text.Split('\n');
  27. int originalTextIndex = 0;
  28. foreach (string line in lines)
  29. {
  30. string s = line;
  31. if (s.Length > 0 && s[s.Length - 1] == '\r')
  32. s = s.Remove(s.Length - 1);
  33. paragraphs.Add(new Paragraph(s, originalTextIndex));
  34. originalTextIndex += line.Length + 1;
  35. }
  36. }
  37. private void DrawBlockAlign(string text, Graphics g, Font font, Brush textBrush,
  38. RectangleF textRect, StringFormat format, HorzAlign horzAlign, float lineHeight, bool forceJustify)
  39. {
  40. bool wordWrap = (format.FormatFlags & StringFormatFlags.NoWrap) == 0;
  41. StringFormatFlags saveFlags = format.FormatFlags;
  42. format.FormatFlags |= StringFormatFlags.NoWrap;
  43. if (lineHeight == 0)
  44. lineHeight = font.GetHeight(g);
  45. // wrap words
  46. int charactersFitted;
  47. float height = CalcHeight(text, g, font, textRect.Width, textRect.Height, horzAlign,
  48. lineHeight, forceJustify, (format.FormatFlags & StringFormatFlags.DirectionRightToLeft) > 0,
  49. wordWrap, format.Trimming, out charactersFitted);
  50. // calculate offset if line alignment is not Near
  51. float offsetY = textRect.Top;
  52. if (format.LineAlignment == StringAlignment.Center)
  53. offsetY = textRect.Top + (textRect.Height - height) / 2;
  54. else if (format.LineAlignment == StringAlignment.Far)
  55. offsetY = textRect.Top + (textRect.Height - height) - 1;
  56. // set clip. needed if amount of text greather than textRect.Height
  57. Region saveClip = g.Clip;
  58. g.SetClip(textRect, CombineMode.Intersect);
  59. // draw each paragraph
  60. foreach (Paragraph paragraph in paragraphs)
  61. {
  62. paragraph.Draw(g, font, textBrush, textRect.Left, offsetY, textRect.Width, textRect.Height, format);
  63. }
  64. g.SetClip(saveClip, CombineMode.Replace);
  65. format.FormatFlags = saveFlags;
  66. }
  67. private void DrawNormalText(string text, Graphics g, Font font, Brush brush, RectangleF rect,
  68. StringFormat format, HorzAlign horzAlign, float fontWidthRatio, float lineHeight,
  69. bool wysiwyg, bool forceJustify)
  70. {
  71. GraphicsState state = g.Save();
  72. g.ScaleTransform(fontWidthRatio, 1);
  73. rect.X /= fontWidthRatio;
  74. rect.Width /= fontWidthRatio;
  75. if (horzAlign == HorzAlign.Justify || wysiwyg || lineHeight != 0)
  76. DrawBlockAlign(text, g, font, brush, rect, format, horzAlign, lineHeight, forceJustify);
  77. else
  78. g.DrawString(text, font, brush, rect, format);
  79. g.Restore(state);
  80. }
  81. private void DrawRotatedText(string text, Graphics g, Font font, Brush brush, RectangleF rect,
  82. StringFormat format, HorzAlign horzAlign, float fontWidthRatio, float lineHeight, int angle,
  83. bool wysiwyg, bool forceJustify)
  84. {
  85. GraphicsState state = g.Save();
  86. Region saveClip = g.Clip;
  87. g.SetClip(rect, CombineMode.Intersect);
  88. g.TranslateTransform(rect.Left + rect.Width / 2, rect.Top + rect.Height / 2);
  89. g.RotateTransform(angle);
  90. rect.X = -rect.Width / 2;
  91. rect.Y = -rect.Height / 2;
  92. if ((angle >= 90 && angle < 180) || (angle >= 270 && angle < 360))
  93. rect = new RectangleF(rect.Y, rect.X, rect.Height, rect.Width);
  94. g.ScaleTransform(fontWidthRatio, 1);
  95. rect.X /= fontWidthRatio;
  96. rect.Width /= fontWidthRatio;
  97. if ((angle == 0 || angle == 90 || angle == 180 || angle == 270) &&
  98. (horzAlign == HorzAlign.Justify || wysiwyg || lineHeight != 0))
  99. DrawBlockAlign(text, g, font, brush, rect, format, horzAlign, lineHeight, forceJustify);
  100. else
  101. g.DrawString(text, font, brush, rect, format);
  102. g.Restore(state);
  103. g.SetClip(saveClip, CombineMode.Replace);
  104. }
  105. #endregion
  106. #region Public Methods
  107. /// <summary>
  108. /// Draws a string.
  109. /// </summary>
  110. /// <param name="text">String to draw.</param>
  111. /// <param name="g"><b>Graphics</b> object to draw on.</param>
  112. /// <param name="font">Font that used to draw text.</param>
  113. /// <param name="brush">Brush that determines the color and texture of the drawn text. </param>
  114. /// <param name="rect"><b>RectangleF</b> structure that specifies the location of the drawn text.</param>
  115. /// <param name="format">StringFormat that specifies formatting attributes, such as line spacing and alignment, that are applied to the drawn text.</param>
  116. /// <param name="horzAlign">Horizontal alignment of the text.</param>
  117. /// <param name="fontWidthRatio">Width ratio of the font used to draw a string.</param>
  118. /// <param name="lineHeight">Line height, in pixels.</param>
  119. /// <param name="angle">Angle of the text, in degrees.</param>
  120. /// <param name="wysiwyg">Indicates whther to draw string close to the printout.</param>
  121. /// <param name="forceJustify">Force justify for the last line.</param>
  122. public void Draw(string text, Graphics g, Font font, Brush brush, RectangleF rect, StringFormat format,
  123. HorzAlign horzAlign, float fontWidthRatio, float lineHeight, int angle, bool wysiwyg, bool forceJustify)
  124. {
  125. angle %= 360;
  126. if (angle == 0)
  127. DrawNormalText(text, g, font, brush, rect, format, horzAlign, fontWidthRatio, lineHeight, wysiwyg, forceJustify);
  128. else
  129. DrawRotatedText(text, g, font, brush, rect, format, horzAlign, fontWidthRatio, lineHeight, angle, wysiwyg, forceJustify);
  130. }
  131. // Wraps the text and calculates its height, in pixels.
  132. public float CalcHeight(string text, Graphics g, Font font, float width, float lineHeight, bool wordWrap)
  133. {
  134. int charactersFit;
  135. return CalcHeight(text, g, font, width, 1000000, HorzAlign.Left, lineHeight, false, false,
  136. wordWrap, StringTrimming.None, out charactersFit);
  137. }
  138. // Wraps the text and calculates its height, in pixels.
  139. public float CalcHeight(string text, Graphics g, Font font, float width, float height, HorzAlign horzAlign,
  140. float lineHeight, bool forceJustify, bool rightToLeft, bool wordWrap, StringTrimming trimming)
  141. {
  142. int charactersFit;
  143. return CalcHeight(text, g, font, width, height, horzAlign, lineHeight, forceJustify, rightToLeft,
  144. wordWrap, trimming, out charactersFit);
  145. }
  146. // Wraps the text and calculates its height, in pixels.
  147. public float CalcHeight(string text, Graphics g, Font font, float width, float height, HorzAlign horzAlign,
  148. float lineHeight, bool forceJustify, bool rightToLeft, bool wordWrap, StringTrimming trimming,
  149. out int charactersFit)
  150. {
  151. if (lineHeight == 0)
  152. lineHeight = font.GetHeight(g);
  153. // make paragraphs
  154. SplitToParagraphs(text);
  155. charactersFit = -1;
  156. // wrap words in each paragraph
  157. float top = 0;
  158. foreach (Paragraph paragraph in paragraphs)
  159. {
  160. int charsFit = -1;
  161. top = paragraph.Wrap(g, font, top, (int)Math.Round(width), height, horzAlign, lineHeight, forceJustify,
  162. wordWrap, trimming, out charsFit);
  163. if (rightToLeft)
  164. paragraph.ApplyRTL((int)width);
  165. if (charsFit != -1 && charactersFit == -1)
  166. charactersFit = charsFit;
  167. }
  168. // height of the text
  169. if (charactersFit == -1)
  170. charactersFit = text.Length;
  171. return top;
  172. }
  173. #endregion
  174. /// <summary>
  175. /// Initializes a new instance of the <b>DrawText</b> class with default settings.
  176. /// </summary>
  177. public DrawText()
  178. {
  179. paragraphs = new List<Paragraph>();
  180. }
  181. }
  182. internal class Paragraph
  183. {
  184. private List<Word> words;
  185. public List<Word> Words
  186. {
  187. get { return words; }
  188. }
  189. private void AlignLine(int startWord, int count, float width, HorzAlign horzAlign)
  190. {
  191. if (count == 0)
  192. return;
  193. int lastWordIndex = startWord + count - 1;
  194. float lineWidth = words[lastWordIndex].Right;
  195. if (horzAlign == HorzAlign.Justify)
  196. {
  197. float delta = (width - lineWidth) / (count - 1);
  198. float curDelta = delta;
  199. for (int i = 1; i < count; i++)
  200. {
  201. words[i + startWord].left += curDelta;
  202. curDelta += delta;
  203. }
  204. }
  205. else
  206. {
  207. float delta = 0;
  208. if (horzAlign == HorzAlign.Center)
  209. delta = (width - lineWidth) / 2;
  210. else if (horzAlign == HorzAlign.Right)
  211. delta = width - lineWidth;
  212. for (int i = 0; i < count; i++)
  213. {
  214. words[i + startWord].left += delta;
  215. }
  216. }
  217. }
  218. private void WrapWord(Graphics g, Font font, float width, int index)
  219. {
  220. Word word = words[index];
  221. TextElementEnumerator charEnum = StringInfo.GetTextElementEnumerator(word.text);
  222. string text = "";
  223. while (charEnum.MoveNext())
  224. {
  225. string textElement = charEnum.GetTextElement();
  226. float textWidth = g.MeasureString(text + textElement, font).Width;
  227. if (textWidth > width)
  228. {
  229. if (text == "")
  230. text = textElement;
  231. Word newWord = new Word(word.text.Substring(text.Length), word.originalTextIndex + text.Length);
  232. word.text = text;
  233. words.Insert(index + 1, newWord);
  234. return;
  235. }
  236. else
  237. text += textElement;
  238. }
  239. }
  240. public void ApplyRTL(float width)
  241. {
  242. foreach (Word word in words)
  243. {
  244. word.left = width - word.left;
  245. }
  246. }
  247. public float Wrap(Graphics g, Font font, float top, float width, float height, HorzAlign horzAlign,
  248. float lineHeight, bool forceJustify, bool wordWrap, StringTrimming trimming, out int charactersFit)
  249. {
  250. float left = 0;
  251. int startWord = 0;
  252. float actualLineHeight = font.GetHeight(g);
  253. string text = "";
  254. charactersFit = -1;
  255. int i = 0;
  256. while (i < words.Count)
  257. {
  258. Word word = words[i];
  259. text += word.text;
  260. float textWidth = g.MeasureString(text, font).Width;
  261. word.left = left;
  262. word.top = top;
  263. word.width = textWidth - left;
  264. // check line fit
  265. if (top + actualLineHeight > height + 0.1f)
  266. charactersFit = word.originalTextIndex;
  267. // word does not fit
  268. if (word.Right > width)
  269. {
  270. int wordsFit = i - startWord;
  271. if (wordWrap)
  272. {
  273. text = "";
  274. left = 0;
  275. if (wordsFit == 0)
  276. {
  277. if (word.text.Length == 1)
  278. {
  279. // it's one-char word, we can't do anything more
  280. startWord = i + 1;
  281. top += lineHeight;
  282. }
  283. else
  284. {
  285. // there is only one word in the line, we need to break it
  286. WrapWord(g, font, width, i);
  287. // continue with the first word
  288. i--;
  289. }
  290. }
  291. else
  292. {
  293. // align the line
  294. AlignLine(startWord, wordsFit, width, horzAlign);
  295. // make it first word in the line and continue with it
  296. startWord = i;
  297. top += lineHeight;
  298. i--;
  299. }
  300. }
  301. else
  302. {
  303. switch (trimming)
  304. {
  305. case StringTrimming.None:
  306. case StringTrimming.Character:
  307. WrapWord(g, font, width - word.left, i);
  308. if (i < words.Count)
  309. words[i + 1].visible = false;
  310. break;
  311. case StringTrimming.Word:
  312. word.visible = false;
  313. break;
  314. case StringTrimming.EllipsisWord:
  315. if (wordsFit > 0)
  316. words[i - 1].text += "...";
  317. word.visible = false;
  318. break;
  319. case StringTrimming.EllipsisCharacter:
  320. WrapWord(g, font, width - word.left - g.MeasureString("..", font).Width, i);
  321. words[i].text += "...";
  322. if (i < words.Count)
  323. words[i + 1].visible = false;
  324. break;
  325. }
  326. break;
  327. }
  328. }
  329. else
  330. {
  331. text += " ";
  332. left = textWidth;
  333. }
  334. i++;
  335. }
  336. // align the rest
  337. AlignLine(startWord, i - startWord, width,
  338. horzAlign == HorzAlign.Justify && !forceJustify ? HorzAlign.Left : horzAlign);
  339. // adjust starting position for the next paragraph
  340. top += lineHeight;
  341. return top;
  342. }
  343. public void Draw(Graphics g, Font font, Brush brush, float offsetX, float offsetY,
  344. float width, float height, StringFormat format)
  345. {
  346. float actualLineHeight = font.GetHeight(g);
  347. StringAlignment saveAlign = format.Alignment;
  348. StringAlignment saveLineAlign = format.LineAlignment;
  349. format.Alignment = StringAlignment.Near;
  350. format.LineAlignment = StringAlignment.Near;
  351. try
  352. {
  353. foreach (Word word in words)
  354. {
  355. if (word.visible)
  356. g.DrawString(word.text, font, brush, word.left + offsetX, word.top + offsetY, format);
  357. }
  358. }
  359. finally
  360. {
  361. format.Alignment = saveAlign;
  362. format.LineAlignment = saveLineAlign;
  363. }
  364. }
  365. public Paragraph(string text, int originalTextIndex)
  366. {
  367. this.words = new List<Word>();
  368. string[] words = text.Split(' ');
  369. string textWithSpaces = "";
  370. foreach (string word in words)
  371. {
  372. if (word == "")
  373. textWithSpaces += " ";
  374. else
  375. {
  376. textWithSpaces += word;
  377. this.words.Add(new Word(textWithSpaces, originalTextIndex));
  378. originalTextIndex += textWithSpaces.Length + 1;
  379. textWithSpaces = "";
  380. }
  381. }
  382. }
  383. }
  384. internal class Word
  385. {
  386. public string text;
  387. public float left;
  388. public float top;
  389. public float width;
  390. public int originalTextIndex;
  391. public bool visible;
  392. public float Right
  393. {
  394. get { return left + width; }
  395. }
  396. public Word(string s, int originalTextIndex)
  397. {
  398. text = s;
  399. this.originalTextIndex = originalTextIndex;
  400. visible = true;
  401. }
  402. }
  403. }