TableView.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Linq;
  5. namespace Terminal.Gui.Views {
  6. /// <summary>
  7. /// View for tabular data based on a <see cref="DataTable"/>
  8. /// </summary>
  9. public class TableView : View {
  10. private int columnOffset;
  11. private int rowOffset;
  12. public DataTable Table { get; private set; }
  13. /// <summary>
  14. /// Zero indexed offset for the upper left <see cref="DataColumn"/> to display in <see cref="Table"/>.
  15. /// </summary>
  16. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  17. public int ColumnOffset {
  18. get {
  19. return columnOffset;
  20. }
  21. //try to prevent this being set to an out of bounds column
  22. set {
  23. //the value before we changed it
  24. var origValue = columnOffset;
  25. columnOffset = Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  26. //if value actually changed we must update UI
  27. if(columnOffset != origValue)
  28. SetNeedsDisplay();
  29. }
  30. }
  31. /// <summary>
  32. /// Zero indexed offset for the <see cref="DataRow"/> to display in <see cref="Table"/> on line 2 of the control (first line being headers)
  33. /// </summary>
  34. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  35. public int RowOffset {
  36. get {
  37. return rowOffset;
  38. }
  39. set {
  40. //the value before we changed it
  41. var origValue = rowOffset;
  42. rowOffset = Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  43. //if value actually changed we must update UI
  44. if(rowOffset != origValue)
  45. SetNeedsDisplay();
  46. }
  47. }
  48. /// <summary>
  49. /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
  50. /// </summary>
  51. public int MaximumCellWidth {get;set;} = 100;
  52. /// <summary>
  53. /// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
  54. /// </summary>
  55. public string NullSymbol {get;set;} = "-";
  56. /// <summary>
  57. /// The symbol to add after each cell value and header value to visually seperate values
  58. /// </summary>
  59. public char SeparatorSymbol {get;set; } = ' ';
  60. /// <summary>
  61. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.
  62. /// </summary>
  63. /// <param name="table">The table to display in the control</param>
  64. public TableView (DataTable table) : base ()
  65. {
  66. this.Table = table ?? throw new ArgumentNullException (nameof (table));
  67. }
  68. ///<inheritdoc/>
  69. public override void Redraw (Rect bounds)
  70. {
  71. Attribute currentAttribute;
  72. var current = ColorScheme.Focus;
  73. Driver.SetAttribute (current);
  74. Move (0, 0);
  75. var frame = Frame;
  76. int activeColor = ColorScheme.HotNormal;
  77. int trackingColor = ColorScheme.HotFocus;
  78. // What columns to render at what X offset in viewport
  79. Dictionary<DataColumn, int> columnsToRender = CalculateViewport(bounds);
  80. Driver.SetAttribute (ColorScheme.HotNormal);
  81. //invalidate current row (prevents scrolling around leaving old characters in the frame
  82. Driver.AddStr(new string (' ',bounds.Width));
  83. // Render the headers
  84. foreach(var kvp in columnsToRender) {
  85. Move (kvp.Value,0);
  86. Driver.AddStr(kvp.Key.ColumnName+ SeparatorSymbol);
  87. }
  88. //render the cells
  89. for (int line = 1; line < frame.Height; line++) {
  90. //invalidate current row (prevents scrolling around leaving old characters in the frame
  91. Move (0,line);
  92. Driver.AddStr(new string (' ',bounds.Width));
  93. //work out what Row to render
  94. var rowToRender = RowOffset + (line-1);
  95. //if we have run off the end of the table
  96. if(rowToRender >= Table.Rows.Count)
  97. continue;
  98. foreach(var kvp in columnsToRender) {
  99. Move (kvp.Value,line);
  100. Driver.AddStr(GetRenderedVal(Table.Rows[rowToRender][kvp.Key]) + SeparatorSymbol);
  101. }
  102. }
  103. void SetAttribute (Attribute attribute)
  104. {
  105. if (currentAttribute != attribute) {
  106. currentAttribute = attribute;
  107. Driver.SetAttribute (attribute);
  108. }
  109. }
  110. }
  111. /// <inheritdoc/>
  112. public override bool ProcessKey (KeyEvent keyEvent)
  113. {
  114. switch (keyEvent.Key) {
  115. case Key.CursorLeft:
  116. ColumnOffset--;
  117. break;
  118. case Key.CursorRight:
  119. ColumnOffset++;
  120. break;
  121. case Key.CursorDown:
  122. RowOffset++;
  123. break;
  124. case Key.CursorUp:
  125. RowOffset--;
  126. break;
  127. case Key.PageUp:
  128. RowOffset -= Frame.Height;
  129. break;
  130. case Key.V | Key.CtrlMask:
  131. case Key.PageDown:
  132. RowOffset += Frame.Height;
  133. break;
  134. case Key.Home | Key.CtrlMask:
  135. RowOffset = 0;
  136. ColumnOffset = 0;
  137. break;
  138. case Key.Home:
  139. ColumnOffset = 0;
  140. break;
  141. case Key.End | Key.CtrlMask:
  142. //jump to end of table
  143. RowOffset = Table.Rows.Count-1;
  144. ColumnOffset = Table.Columns.Count-1;
  145. break;
  146. case Key.End:
  147. //jump to end of row
  148. ColumnOffset = Table.Columns.Count-1;
  149. break;
  150. }
  151. PositionCursor ();
  152. return true;
  153. }
  154. /// <summary>
  155. /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
  156. /// </summary>
  157. /// <param name="bounds"></param>
  158. /// <param name="padding"></param>
  159. /// <returns></returns>
  160. private Dictionary<DataColumn,int> CalculateViewport(Rect bounds, int padding = 1)
  161. {
  162. Dictionary<DataColumn,int> toReturn = new Dictionary<DataColumn, int>();
  163. int usedSpace = 0;
  164. int availableHorizontalSpace = bounds.Width;
  165. int rowsToRender = bounds.Height-1; //1 reserved for the headers row
  166. foreach(var col in Table.Columns.Cast<DataColumn>().Skip(ColumnOffset)) {
  167. toReturn.Add(col,usedSpace);
  168. usedSpace += CalculateMaxRowSize(col,rowsToRender) + padding;
  169. if(usedSpace > availableHorizontalSpace)
  170. return toReturn;
  171. }
  172. return toReturn;
  173. }
  174. /// <summary>
  175. /// Returns the maximum of the <paramref name="col"/> name and the maximum length of data that will be rendered starting at <see cref="RowOffset"/> and rendering <paramref name="rowsToRender"/>
  176. /// </summary>
  177. /// <param name="col"></param>
  178. /// <param name="rowsToRender"></param>
  179. /// <returns></returns>
  180. private int CalculateMaxRowSize (DataColumn col, int rowsToRender)
  181. {
  182. int spaceRequired = col.ColumnName.Length;
  183. for(int i = RowOffset; i<RowOffset + rowsToRender && i<Table.Rows.Count;i++) {
  184. //expand required space if cell is bigger than the last biggest cell or header
  185. spaceRequired = Math.Max(spaceRequired,GetRenderedVal(Table.Rows[i][col]).Length);
  186. }
  187. return spaceRequired;
  188. }
  189. /// <summary>
  190. /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
  191. /// </summary>
  192. /// <param name="value"></param>
  193. /// <returns></returns>
  194. private string GetRenderedVal (object value)
  195. {
  196. if(value == null || value == DBNull.Value)
  197. {
  198. return NullSymbol;
  199. }
  200. var representation = value.ToString();
  201. //if it is too long to fit
  202. if(representation.Length > MaximumCellWidth)
  203. return representation.Substring(0,MaximumCellWidth);
  204. return representation;
  205. }
  206. }
  207. }