ScrollTextBox.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. using System;
  2. using System.ComponentModel;
  3. using System.Windows.Forms;
  4. namespace MarkdownRenderer.Test
  5. {
  6. /// <summary>
  7. /// TextBox with support for getting and setting the vertical scroll bar
  8. /// position, as well as listening to vertical scroll events.
  9. /// </summary>
  10. public class ScrollTextBox : RichTextBox
  11. {
  12. public ScrollTextBox()
  13. {
  14. _components = new Container();
  15. // Calculate width of "W" to set as the small horizontal increment
  16. OnFontChanged(null);
  17. }
  18. [DefaultValue(0)]
  19. [Category("Appearance")]
  20. [Description("Gets or sets the vertical scroll bar's position")]
  21. public int VerticalScrollPosition
  22. {
  23. set => SetScroll(value, Win32.WM_VSCROLL, Win32.SB_VERT);
  24. get => GetScroll(Win32.SB_VERT);
  25. }
  26. [DefaultValue(0)]
  27. [Category("Appearance")]
  28. [Description("Gets or sets the horizontal scroll bar's position")]
  29. public int HorizontalScrollPosition
  30. {
  31. set => SetScroll(value, Win32.WM_HSCROLL, Win32.SB_HORZ);
  32. get => GetScroll(Win32.SB_HORZ);
  33. }
  34. [Description("Fired when the scroll bar's vertical position changes")]
  35. [Category("Property Changed")]
  36. public event ScrollEventHandler ScrollChanged;
  37. // Fire scroll event if the scroll-bars are moved
  38. protected override void WndProc(ref Message message)
  39. {
  40. base.WndProc(ref message);
  41. if (message.Msg == Win32.WM_VSCROLL
  42. || message.Msg == Win32.WM_HSCROLL
  43. || message.Msg == Win32.WM_MOUSEWHEEL
  44. ) TryFireScrollEvent();
  45. }
  46. // Key-down includes navigation keys, but also typing and pasting can
  47. // cause a scroll event
  48. protected override void OnKeyDown(KeyEventArgs e)
  49. {
  50. base.OnKeyDown(e);
  51. TryFireScrollEvent();
  52. }
  53. protected override void OnKeyUp(KeyEventArgs e)
  54. {
  55. base.OnKeyUp(e);
  56. TryFireScrollEvent();
  57. }
  58. // Resizing can alter the word-wrap or fit the content causing the text
  59. // to scroll
  60. protected override void OnResize(EventArgs e)
  61. {
  62. base.OnResize(e);
  63. TryFireScrollEvent();
  64. }
  65. // Clicking an empty space can move the carot to the beginning of the
  66. // line causing a scroll event
  67. protected override void OnMouseDown(MouseEventArgs e)
  68. {
  69. base.OnMouseDown(e);
  70. TryFireScrollEvent();
  71. }
  72. protected override void OnMouseUp(MouseEventArgs e)
  73. {
  74. base.OnMouseUp(e);
  75. TryFireScrollEvent();
  76. }
  77. // If the mouse button is down, a text selection is probably being done
  78. // where dragging the selection can caues scroll events
  79. protected override void OnMouseMove(MouseEventArgs e)
  80. {
  81. base.OnMouseMove(e);
  82. if (e.Button != MouseButtons.None) TryFireScrollEvent();
  83. }
  84. // Changing the size of the font can cause a scroll event. Also, when
  85. // scrolling horizontally, the event will notify whether the scroll
  86. // was a large or small change. For vertical, small increments are 1
  87. // line, but for horizontal, it is several pixels. To guess what a
  88. // small increment is, get the width of the W character and anything
  89. // smaller than that will be represented as a small increment
  90. protected override void OnFontChanged(EventArgs e)
  91. {
  92. base.OnFontChanged(e);
  93. using (var graphics = CreateGraphics()) _fontWidth = (int)graphics.MeasureString("W", Font).Width;
  94. TryFireScrollEvent();
  95. }
  96. protected override void Dispose(bool disposing)
  97. {
  98. if (disposing) _components?.Dispose();
  99. base.Dispose(disposing);
  100. }
  101. private void SetScroll(int value, uint windowsMessage, int scrollBarMessage)
  102. {
  103. Win32.SetScrollPos(Handle, scrollBarMessage, value, true);
  104. Win32.PostMessage(Handle, windowsMessage, 4 + 0x10000 * value, 0);
  105. }
  106. private int GetScroll(int scrollBarMessage) => Win32.GetScrollPos(Handle, scrollBarMessage);
  107. // Fire both horizontal and vertical scroll events seperately, one
  108. // after the other. These first test if a scroll actually occurred
  109. // and won't fire if there was no actual movement
  110. private void TryFireScrollEvent()
  111. {
  112. // Don't do anything if there is no event handler
  113. if (ScrollChanged == null) return;
  114. TryFireHorizontalScrollEvent();
  115. TryFireVerticalScrollEvent();
  116. }
  117. private void TryFireHorizontalScrollEvent()
  118. {
  119. // Don't do anything if there is no event handler
  120. if (ScrollChanged == null) return;
  121. var lastScrollPosition = _lastHorizontalScrollPosition;
  122. var scrollPosition = HorizontalScrollPosition;
  123. // Don't do anything if there was no change in position
  124. if (scrollPosition == lastScrollPosition) return;
  125. _lastHorizontalScrollPosition = scrollPosition;
  126. ScrollChanged(this
  127. , new ScrollEventArgs(
  128. scrollPosition < lastScrollPosition - _fontWidth
  129. ? ScrollEventType.LargeDecrement
  130. : scrollPosition > lastScrollPosition + _fontWidth
  131. ? ScrollEventType.LargeIncrement
  132. : scrollPosition < lastScrollPosition
  133. ? ScrollEventType.SmallDecrement
  134. : ScrollEventType.SmallIncrement
  135. , lastScrollPosition
  136. , scrollPosition
  137. , ScrollOrientation.HorizontalScroll
  138. )
  139. );
  140. }
  141. private void TryFireVerticalScrollEvent()
  142. {
  143. // Don't do anything if there is no event handler
  144. if (ScrollChanged == null) return;
  145. var lastScrollPosition = _lastVerticalScrollPosition;
  146. var scrollPosition = VerticalScrollPosition;
  147. // Don't do anything if there was no change in position
  148. if (scrollPosition == lastScrollPosition) return;
  149. _lastVerticalScrollPosition = scrollPosition;
  150. ScrollChanged(this
  151. , new ScrollEventArgs(
  152. scrollPosition < lastScrollPosition - 1
  153. ? ScrollEventType.LargeDecrement
  154. : scrollPosition > lastScrollPosition + 1
  155. ? ScrollEventType.LargeIncrement
  156. : scrollPosition < lastScrollPosition
  157. ? ScrollEventType.SmallDecrement
  158. : ScrollEventType.SmallIncrement
  159. , lastScrollPosition
  160. , scrollPosition
  161. , ScrollOrientation.VerticalScroll
  162. )
  163. );
  164. }
  165. private int _lastVerticalScrollPosition;
  166. private int _lastHorizontalScrollPosition;
  167. private int _fontWidth;
  168. private IContainer _components = null;
  169. private static class Win32
  170. {
  171. public const uint WM_HSCROLL = 0x114;
  172. public const uint WM_VSCROLL = 0x115;
  173. public const uint WM_MOUSEWHEEL = 0x20A;
  174. public const int SB_VERT = 0x1;
  175. public const int SB_HORZ = 0x0;
  176. [System.Runtime.InteropServices.DllImport("user32.dll")]
  177. public static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw);
  178. [System.Runtime.InteropServices.DllImport("user32.dll")]
  179. public static extern int GetScrollPos(IntPtr hWnd, int nBar);
  180. [System.Runtime.InteropServices.DllImport("User32.Dll", EntryPoint = "PostMessageA")]
  181. public static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);
  182. }
  183. }
  184. }