带自定义滚动条的 Listview
本文展示了如何将默认的 listview 滚动条替换为自定义滚动条,

引言
我偶尔会看到有人发帖询问是否可以替换ListView内置的滚动条。回答五花八门,从“不可能”到一些不错的技巧都有。也许我错过了什么,但我还没有找到一个可以直接“下载-解压-运行”的解决方案。 :-) 另外,我(认为)没有一篇文章提到如何附加自定义滚动条,而这里的自定义滚动条,我不是指用户绘制的Windows滚动条。
给心急的人
我创建了一个自定义的、可风格化的滚动条控件和一个自定义的ListView
控件。后者继承自原始控件,但能够附加一个或多个自定义滚动条。
样式代码封装在单独的绘图器(painter)中,可以在其他项目或其他用户控件中重用。但如果你对绘图器感兴趣,请继续阅读。
心急的朋友,可以在git clone git://github.com/deveck/Deveck.Utils.git 查看代码。
在Program.cs文件中取消注释
Example_ScrollbarTest();
以启用滚动条示例并运行它。
绘图器、滚动条和ListView的代码位于
Deveck.Utils/Ui/Painters
Deveck.Utils/Ui/Scrollbar
Deveck.Utils/Ui
示例将三个样式略有不同的滚动条附加到一个ListView
上。
详细说明
接下来的章节将详细介绍组件和实现。
绘图器
绘图器不直接与滚动条、ListView或其他任何东西关联。它们通常只是绘制一些东西,或者在特殊情况下,它们会将过滤后的输入数据转发给另一个绘图器。
我将绘图封装起来是因为我希望它可以重用,例如,当前包含的绘图器不仅用于绘制滚动条,还用于绘制各种按钮,但它们目前不属于Deveck.Utils
。
绘图器接口看起来非常简单。
public abstract class Painter
{
public enum State
{
Normal,
Pressed,
Hover
}
public abstract void Paint(Graphics g, Rectangle position,
State state, string text, Image img, Font textFont, Rectangle? referencePosition);
}
Rectangle position
:此绘图器可以使用的矩形区域。State state
:几种状态之一(正常、悬停、按下),这可能会影响使用的颜色。string text
:要打印的文本。绘图器不一定需要打印文本,因为可能存在另一个链式绘图器。Image img
:与目标关联的图像。绘图器不一定需要绘制图像。Rectangle? referencePosition
:此矩形用于字体缩放。如果提供,则提供的textFont
是referencePosition
大小的字体,需要缩放到position
。
目前可用的绘图器有
Office2007BlackButtonPainter
Office2007BlueButtonPainter
WindowsStyledButtonPainter
SymbolPainter
还有一些特殊的绘图器,它们会过滤输入数据,因此被称为过滤器绘图器。
PainterFilterNoText
:移除文本并调用后续的绘图器PainterFilterSize
:调整传递的边界,例如添加填充并居中、最大尺寸或按指定比例拉伸,然后调用后续的绘图器
另一个特殊的绘图器是
StackedPainter
它接受多个绘图器或过滤器,并按指定的顺序调用它们。
滚动条
自定义滚动条需要实现一个简单的接口ICustomScrollbar
,如下所示。它们不一定需要继承自Control
,但可能需要;)。该接口应该是不言自明的。看看CustomScrollbar
,它相当直接,有向上和向下的按钮用于小范围滚动,有可滑动的滑块用于方便快速的鼠标滚动,以及通过点击向上-向下按钮和滑块之间的区域进行大范围滚动,就像普通的滚动条一样。
public delegate void ScrollValueChangedDelegate(ICustomScrollbar sender, int newValue);
public interface ICustomScrollbar
{
event ScrollValueChangedDelegate ValueChanged;
int LargeChange { get; set; }
int SmallChange { get; set; }
int Maximum { get; set; }
int Minimum { get; set; }
int Value { get; set; }
}
自定义ListView
自定义ListView稍微棘手一些,需要了解Windows GUI的底层工作原理,而不是仅仅依赖漂亮的Designer。幸运的是,.NET为我们提供了WndProc
方法,我们可以在其中捕获所有感兴趣的事件,但稍后会详细介绍。
首先,我们需要定义对默认ListView控件需要进行的更改。
- 我们需要一种可靠的方式来通过代码滚动列表
- 我们需要一种方法来获取当前的滚动值和可见项的数量
- 我们需要摆脱内置的滚动条
- 我们需要在所有应该改变滚动值的操作上收到通知,以便更新滚动条
- 我们需要将所有这些整合在一起:)
如何通过代码滚动列表
解决这个问题的第一个方法是简单地设置列表的TopItem
,这样做有时有效,但并非总是如此。深入研究会发现,.NET的listview
包装器只是在猜测滚动值,因为本地的listview
不支持这样的操作。那么解决方案是什么呢?多次设置TopItem
,直到正确的项位于顶部,是的,这是一个丑陋的hack;)
public void SetScrollPosition(int pos)
{
pos = Math.Min(Items.Count - 1, pos);
if (pos < 0 || pos >= Items.Count)
return;
EnsureVisible(pos);
for (int i = 0; i < 10; i++)
{
if(TopItem != null && TopItem.Index != pos)
TopItem = Items[pos];
}
}
如何获取当前的滚动值和可见项的数量
乍一看,这似乎与第一个问题完全相同。我们可以通过查询TopItem
来获取当前的滚动值,但我们还需要最大值、最小值和变化值。因此,我们需要P/Invoke调用getScrollInfo
方法,该方法返回一个SCROLLINFO
结构。这个结构还包含变化值,幸运的是,大变化值等于可见项的数量,因为当处理大变化时,列表总是滚动一整页。
public void GetScrollPosition
(out int min, out int max, out int pos, out int smallchange, out int largechange)
{
SCROLLINFO scrollinfo = new SCROLLINFO();
scrollinfo.cbSize = (uint)Marshal.SizeOf(typeof(SCROLLINFO));
scrollinfo.fMask = (int)ScrollInfoMask.SIF_ALL;
if (GetScrollInfo(this.Handle, (int)SBTYPES.SB_VERT, ref scrollinfo))
{
min = scrollinfo.nMin;
max = scrollinfo.nMax;
pos = scrollinfo.nPos;
smallchange = 1;
largechange = (int)scrollinfo.nPage;
}
else
{
min = 0;
max = 0;
pos = 0;
smallchange = 0;
largechange = 0;
}
}
如何移除内置滚动条
你可能会像我一样认为,这是最简单的任务。只需设置
list.Scrollable = false;
然后就完成了。这确实隐藏了滚动条,但让你无法通过代码滚动列表。因此,这使得前面提到的WndProc
派上用场。下面的代码块只显示了WndProc
的一个片段,请查看源代码以获取完整方法。
我们需要捕获'calcsize
'消息,检索窗口样式,并在激活滚动条时禁用它。
protected override void WndProc(ref Message m)
{
...
if (m.Msg == WM_NCCALCSIZE) // WM_NCCALCSIZE
{
int style = (int)GetWindowLong(this.Handle, GWL_STYLE);
if ((style & WS_VSCROLL) == WS_VSCROLL)
SetWindowLong(this.Handle, GWL_STYLE, style & ~WS_VSCROLL);
}
...
}
如何获得所有感兴趣操作的通知
我们现在几乎完成了,只需要在添加或删除项、列表被清空或列表通过其他机制滚动时更新滚动条。所以我们再次挂钩到WndProc
方法,并捕获相应的消息。
protected override void WndProc(ref Message m)
{
...
if (m.Msg == WM_VSCROLL)
{
int max, min, pos, smallchange, largechange;
GetScrollPosition(out min, out max, out pos, out smallchange, out largechange);
if (ScrollPositionChanged != null)
ScrollPositionChanged(this, pos);
if (_vScrollbar != null)
_vScrollbar.Value = pos;
}
else if (m.Msg == LVM_INSERTITEMA || m.Msg == LVM_INSERTITEMW)
OnItemAdded();
else if (m.Msg == LVM_DELETEITEM || m.Msg == LVM_DELETEALLITEMS)
OnItemsRemoved();
...
}
结果
它运行得非常好,看起来也很棒。查看源代码并留下你的评论。
可以访问我的博客获取更多信息。
历史
- 2011年8月2日 - 初始版本