65.9K
CodeProject 正在变化。 阅读更多。
Home

带自定义滚动条的 Listview

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (13投票s)

2011年8月4日

Apache

5分钟阅读

viewsIcon

64330

downloadIcon

6503

本文展示了如何将默认的 listview 滚动条替换为自定义滚动条, 并展示了自定义滚动条的实现。

Listview in action

引言

我偶尔会看到有人发帖询问是否可以替换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:此矩形用于字体缩放。如果提供,则提供的textFontreferencePosition大小的字体,需要缩放到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日 - 初始版本
© . All rights reserved.