一、有关滚动条的WinApi
自定义滚动条前,我们需要了解下系统滚动条的相关信息。想必大家已经了解过这部分内容,不过还是再熟悉下,重绘滚动条这个很重要哦!
由系统绘制的滚动条都可以通过可以由WinApi中的GetScrollInfo()函数获取滚动信息。
查阅WinApi手册后发现,获取的信息在SCROLLINFO结构中,具体SCROLLINFO结构成员含义如下:
cbSize:标识结构体大小
fMask:说明待找回的滚动条参数(通俗点说:我们需要获取滚动条什么参数就在这里按要求设置哪个值),以下为它可设置的值:
SIF_PAGE:获取SCROLLINFO结构中的nPage成员
SIF_POS:获取SCROLLINFO结构中的nPos成员
SIF_RANGE:获取SCROLLINFO结构中的nMin和nMax成员
SIF_TRACKPOS:获取SCROLLINFO 结构中的nPage成员
nMax:滚动范围最大值
nMin:滚动范围最小值
nPage:页尺寸,确定比例滚动条滑块的大小(编辑框中一页可显示的行数(不足一页时为当前页中的行数))
nPos:滚动条滑块的位置
nTrackPos:拖动时滚动条滑块的位置(已经滚动的行数)
二、方案选取
了解了以上内容后,我们开始来绘制滚动条!
有几种方法绘制滚动条:
1)采用Image做滚动条的上下箭头和中间滑块。
2)采用PictureBox做为滚动条中间滑块,在按需要向PictureBox中加载图片。
与原始滚动条联动的方法:
1)直接获取原始滚动条的位置。
2)使用WinApi截取原始滚动条消息设置滚动条位置后再发送消息。
在绘制滚动条的方案上,采用Image可以直接用图标做为滚动条组件,但是Image在设置大小上属性并没有开放,而且当我们没有给Image设置图标时,会报错。而PictureBox也能加载图标,而且不加载图标时他使用的是底色做为背景,比较灵活,更重要的是PictureBox的大小可以自由设置,非常方便。所以我们采用PictureBox做滚动条组件。
与滚动条联动的方案上,方案一需要当原始滚动条的位置可获取的情况下才方便使用,但是大部分控件的滚动条都是由系统绘制的,所以很难直接得到。方案二中通过WinApi截取滚动条消息对于大部分控件的滚动条都适用(DataGridView除外,后文会讲到),只是我们需要熟悉下WinApi中的一些用法。由此我们采用方案二。
所以,最终我们采用以PictureBox做为滚动条组件,WinApi截取滚动条消息的方法使自定义滚动条与原始滚动条联动,噢!对了,还有就是在实际使用中记得要用自定义滚动条将原始滚动条覆盖掉哦!
三、具体实现
通过图所示分析我们发现滚动条与控件有如下比例关系:
滑块的高度 / 滚动条除去上下箭头的高度 = 编辑框一页容纳的行数 / 编辑框文本总行数
已滚动行数 / 需要滚动才能显示的总行数 = 滑块最上端的位置 / 滚动条空白区域的总高度
由这两个比例关系我们就可以计算出滚动条滑块的高度和位置:
滑块高度 = 滚动条除去上下箭头的高度 * 编辑框一页容纳的行数 / 编辑框文本总行数
滑块最上端的位置 = 滚动条空白区域的总高度 * 已滚动行数 / 需要滚动才能显示的总行数
其中的几个值我们都可以求得:
滚动条除去上下箭头的高度:Height = ScrollBar.Height - ScrollBar.upArrow.Height - ScrollBar.downArrow.Height
滚动条空白区域的总高度:EmptyHeight = ScrollBar.Height - ScrollBar.upArrow.Height - ScrollBar.downArrow.Height - ScrollBar.Thumb.Height
编辑框一页容纳的行数:nPage
编辑框文本总行数:nMax - nMin + 1
已滚动行数:nTrackPos
需要滚动才能显示的总行数: nMax - nMin + 1 - nPage
里面的几个滚动条参数已经在文章开头讲过了,这里就不再重复。具体如何截取WinApi消息,直接上代码,这是参考了其他网友的博客。
代码如下:
1 public class Win32Api { 2 [StructLayout(LayoutKind.Sequential)] 3 public struct TagScrollinfo { 4 public uint cbSize; 5 public uint fMask; 6 public int nMin; 7 public int nMax; 8 public uint nPage; 9 public int nPos; 10 public int nTrackPos; 11 } 12 13 public enum fnBar { 14 SB_HORZ = 0, 15 SB_VERT = 1, 16 SB_CTL = 2 17 } 18 19 public enum fMask { 20 SIF_ALL, 21 SIF_DISABLENOSCROLL = 0X0010, 22 SIF_PAGE = 0X0002, 23 SIF_POS = 0X0004, 24 SIF_RANGE = 0X0001, 25 SIF_TRACKPOS = 0X0008 26 } 27 28 public static int MakeLong(short lowPart, short highPart) { 29 return (int)(((ushort)lowPart) | (uint)(highPart << 16)); 30 } 31 32 public const int SB_THUMBTRACK = 5; 33 public const int WM_HSCROLL = 0x114; 34 public const int WM_VSCROLL = 0x115; 35 36 [DllImport("user32.dll", EntryPoint = "GetScrollInfo")] 37 public static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref Scrollinfo lpsi); 38 39 [DllImport("user32.dll", EntryPoint = "SetScrollInfo")] 40 public static extern int SetScrollInfo(IntPtr hwnd, int fnBar, [In] ref Scrollinfo lpsi, bool fRedraw); 41 42 [DllImport("User32.dll", CharSet = CharSet.Auto, EntryPoint = "SendMessage")] 43 public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); 44 45 //[DllImport("user32.dll", SetLastError = true)] 46 [DllImport("user32.dll", CallingConvention = CallingConvention.Winapi)] 47 public static extern bool PostMessage(IntPtr hWnd, uint Msg, long wParam, int lParam); 48 49 public struct Scrollinfo { 50 public uint cbSize; 51 public uint fMask; 52 public int nMin; 53 public int nMax; 54 public uint nPage; 55 public int nPos; 56 public int nTrackPos; 57 } 58 59 public enum ScrollInfoMask { 60 SIF_RANGE = 0x1, 61 SIF_PAGE = 0x2, 62 SIF_POS = 0x4, 63 SIF_DISABLENOSCROLL = 0x8, 64 SIF_TRACKPOS = 0x10, 65 SIF_ALL = SIF_RANGE + SIF_PAGE + SIF_POS + SIF_TRACKPOS 66 } 67 68 public enum ScrollBarDirection { 69 SB_HORZ = 0, 70 SB_VERT = 1, 71 SB_CTL = 2, 72 SB_BOTH = 3 73 } 74 }
有了这个类,我们就可以方便的获取滚动条的消息了,如下方法获取,这里以TreeView为例,其他控件改this.TreeView.Handle就可以了。
代码如下:
1 /// <summary> 2 /// 获取滚动条消息 3 /// </summary> 4 /// <returns></returns> 5 private Win32Api.Scrollinfo GetScrollInfo() 6 { 7 Win32Api.Scrollinfo si = new Win32Api.Scrollinfo(); 8 si.cbSize = (uint)Marshal.SizeOf(si); 9 si.fMask = (int)(Win32Api.ScrollInfoMask.SIF_DISABLENOSCROLL | Win32Api.ScrollInfoMask.SIF_ALL); 10 Win32Api.GetScrollInfo(this.TreeView.Handle, (int)Win32Api.ScrollBarDirection.SB_VERT, ref si); 11 return si; 12 }
至此,我们就可以绘制出和原始滚动条成比例的一个自定义滚动条了。对于滚动条的联动,我们还需要做点工作,继续往下看。
当我们拖动滑块时,需要向原始滚动条发送滚动参数才能让它移动。不多说了,直接看代码吧!
代码如下:
1 /// <summary> 2 /// 设置控件的滚动条的位置 3 /// </summary> 4 /// <param name="scrollInfo"></param> 5 /// <param name="pos"></param> 6 private void SetControlScrollLocation(Win32Api.Scrollinfo scrollInfo, int pos) { 7 Win32Api.Scrollinfo info = scrollInfo; 8 info.nPos = pos; 9 Win32Api.SetScrollInfo(this.TreeView.Handle, (int)Win32Api.ScrollBarDirection.SB_VERT, ref info, true); 10 IntPtr aa = new IntPtr(Win32Api.MakeLong((short)Win32Api.SB_THUMBTRACK, (short)(info.nPos))); 11 Win32Api.SendMessage(this.STreeView.Handle, Win32Api.WM_VSCROLL, aa, new IntPtr()); 12 }
这样我们就实现了通过截取WinApi消息来实现自定义滚动条了。
还有一个问题需要处理下,我们拖动滑块时会经常出现自定义滚动条已经滑到最低端时原始滚动条还没有到低端或者已经提前到达了低端,这个问题当时我也弄了好久才明白,原来是滚动比例的问题。
解决办法:在设置原始滚动条位置前先将其位置乘以一下比例:
原始滚动条的最低位置与自定义滚动条的最低位置比例:(nMax - nMin + 1 - nPage) / (ScrollBar.Height - ScrollBar.upArrow.Height - ScrollBar.downArrow.Height - ScrollBar.Thumb.Height)
关于DataGridView滚动条的绘制
在绘制DataGridView滚动条的时候,本以为跟TreeView、ListBox这些控件的滚动条绘制方法一样,没想到当我去读滚动条消息的时候始终取不到值,在网上搜了很多关于滚动条绘制的文章都没有提到具体方法,都说通过WinApi来截取消息。经过我的多方求证才发现Winform里的DataGridView滚动条不是由系统直接绘制的,而是通过单独的滚动条组合而成的,这个在C#源码里也证实了这一点(现在C#源码开源了哦)。
然而也不能直接获取它的滚动条信息。但是DataGridView有些索引功能可以让我们绕过它的滚动条问题。
具体如下:
还是根据前面讲到的比例关系计算滑块的高度和位置,前面需要截取到的滚动条消息都可以通过DataGridView的一些值代替,下面还是把各个必要的值列出来:
滚动条除去上下箭头的高度: Height = ScrollBar.Height - ScrollBar.upArrow.Height - ScrollBar.downArrow.Height
滚动条空白区域的总高度: EmptyHeight = ScrollBar.Height - ScrollBar.upArrow.Height - ScrollBar.downArrow.Height - ScrollBar.Thumb.Height
DataGridView的总高度(编辑框一页容纳的行数): DataGridView.Height
编辑框文本总行数: DataGridView.Rows.GetRowsHeight(DataGridViewElementStates.None) - this.SDataGridView.ColumnHeadersHeight
显示框第一行的索引(已滚动行数): this.SDataGridView.FirstDisplayedScrollingRowIndex
需要滚动才能显示的总行数: DataGridView.RowCount - this.SDataGridView.Rows.GetRowCount(DataGridViewElementStates.Displayed)
这样,我们也可以通过这些值来得到滑块的高度和位置了!
但是,怎么去设置原始滚动条滚动的位置呢?
幸好DataGridView有一个可读可写的属性FirstDisplayedScrollingRowIndex,它可以获取或设置显示在DataGridView上第一行的索引。当我们需要设置原始滚动条的位置时,我们只需要设置FirstDisplayedScrollingRowIndex的值了。如有不明白的可以结合代码看下就理解了。
由于代码太多,文章中没有把具体代码附上,具体源代码在GitHub上,希望大家一起参与进来升级改进。
GitHub地址:https://github.com/Glf9832/CustomScrollBar.git
同时还要感谢网友们的文章参考,才能让我学习到这么多知识!
参考文献:
1、[原创]WinForm中重绘滚动条以及用重绘的滚动条控制ListBox的滚动
2、winform:关于滚动条的美化
3、自己开发基于c#的垂直滚动条控件
4、How to skin scrollbars for Panels, in C#