如何为 C# 中的 Panels 控件换肤滚动条






4.90/5 (59投票s)
本文介绍如何创建一个可换肤的滚动条作为用户控件,并在 Panel 中使用它来替换丑陋的 Windows 滚动条。
|
![]() |
引言
您是否曾想摆脱那个丑陋的 Windows 滚动条?我曾经很多次想这样做。直到现在,这仍然是一项极其繁琐且困难的任务。在 .NET 出现之前,要摆脱 listview、list control 以及任何具有默认 Windows 滚动条的控件的默认滚动条是一项巨大的工程。如果您想看看有多困难,可以看看我为 Visual C++ 6.0 MFC 撰写的另一篇文章,如何为 CListCtrl 换肤,包括滚动条和列标题。
幸运的是,我们现在有了 .NET 框架中的用户控件和面板。这大大简化了事情,但要自定义 Windows 滚动条的外观和感觉并让它们正常工作,仍然不那么容易或直接。
入门
首先,我们将创建一个名为 TestApp 的新的 C# Windows 应用程序项目。在设计模式下打开 Form1.cs,添加一个高度为 179 像素的 Panel
控件,并将其命名为 outerPanel
。然后,创建另一个 Panel
,这次在 OuterPanel
内部创建,并将其命名为 InnerPanel
。现在,将 innerPanel
的 AutoScroll
属性设置为 True
。
接下来,我们要向 innerPanel
中添加某种控件,以便我们能够滚动以测试我们的自定义滚动条。由于某种原因,我们无法使用设计器将控件添加到 innerPanel
,因为它会弄乱 DisplayRectangle
属性并且不会返回正确的值。所以我们必须通过代码将控件添加到 innerPanel
,这样一切都会正常工作。
因此,在“查看代码”模式下打开 Form.cs,然后在 InitializeCompontent();
调用正下方插入以下代码
public Form1()
{
InitializeComponent();
Button b = new Button();
b.Location = new Point(0, 900);
b.Text = "test";
this.innerPanel.Controls.Add(b);
}
这将会在 innerPanel
的顶部向下 900 像素处添加一个按钮,当我们运行应用程序时,应该会出现一个滚动条。
换肤滚动条(如果这么简单就好了 :p)
现在,我们有了一个可以滚动的 Panel
,但它很丑。那么,我们如何给它换肤呢?据我所知,实际上没有办法给 Windows 滚动条换肤。所以我们必须创建一个自己的滚动条作为用户控件。基本上,我们必须完全模仿 Windows VScroll 控件的功能,但要添加使用图形作为箭头、通道和滑块控件的能力。然后,稍后我们将编写更多代码来隐藏面板滚动条,并让我们的自定义用户控件滚动条控制面板的滚动。
我们首先向解决方案添加一个新的控件库项目,并将默认添加的用户控件重命名为 CustomScrollbar
。
现在,我们开始创建图形并编写代码,以使我们的 CustomScrollbar
看起来和功能符合我们的期望。
创建滚动条图形
所以,首先,我们必须弄清楚如何以一种易于在大多数情况下重用的方式创建图形。我知道滚动条总是具有四个通用属性
- 向上箭头
- 向下箭头
- 滑块控件
- 通道
这很好,所以我们只需要能够提供向上箭头图形、向下箭头图形、滑块控件图形和通道图形/颜色即可。
但是,还有一件事……标准 Windows 滚动条的滑块控件的大小总是根据 Minimum
/Maximum
和 LargeChange
属性进行适当调整。这意味着我们必须赋予我们的滑块控件动态更改大小的能力。是的,这并不有趣,因为为了使用图形来实现这一点,我们必须将滑块控件图形分成五个不同的图形,其中两个需要根据滑块控件的大小进行拉伸。以下是我划分图形的方式
因此,如您所见,我们将拥有以下图形,可以通过用户控件的属性面板进行自定义。
- 向上箭头图像
- 向下箭头图像
- 滑块顶部图像
- 滑块顶部拉伸图像
- 滑块中间图像
- 滑块底部拉伸图像
- 滑块底部图像
在此实现中,我将通道部分实现为纯色而不是图形,仅用于演示。如果您愿意,可以很容易地将其更改为使用图像。顺便说一下,为箭头和滑块控件添加鼠标悬停图像的功能也非常容易,但我不会涵盖这一点。我会将其留作给所有开发人员的练习 :)
为滚动条控件准备属性和事件
现在,我们已经弄清楚了图形。我们只需要确定滚动条的功能。我不喜欢重新发明轮子或改变我习惯的事物。所以,我将使我们的自定义滚动条的运行方式与 VScroll 或 Windows 滚动条控件**完全相同**,这样如果我想用我的自定义滚动条替换一个丑陋的 Windows 滚动条,我就可以简单地将它们交换,它就能工作。
因此,我的控件将具有以下公开属性
Maximum
-int
Minimum
-int
Value
-int
LargeChange
-int
SmallChange
-int
以及以下事件
Scroll
ValueChanged
以及以下自定义属性,以方便我们换肤
ChannelColor
-Color
DownArrowImage
-Image
ThumbBottomImage
-Image
ThumbBottomSpanImage
-Image
ThumbMiddleImage
-Image
ThumbTopImage
-Image
ThumbTopSpanImage
-Image
UpArrowImage
-Image
通过在用户控件中正确实现所有这些属性,我们将拥有一个完全可自定义的滚动条,它的功能与常规 Windows 滚动条完全相同,而且它还可以根据我们的喜好变得很酷或不酷 :)
实现滚动条用户控件
现在,这是棘手的部分。我们必须编写自己的滚动条控件。这不一定是一项容易完成的任务。但是,一旦您完成了这项工作,您应该永远不必再编写另一个了,即使您这样做,至少您将拥有我们在这里开发的产品的代码库。
为了制作我们的自定义滚动条,我们需要重写和/或响应以下事件
OnPaint
- 重写MouseUp
- 处理MouseDown
- 处理MouseMove
- 处理
首先,我们将实例化所有变量。有些是 protected
,稍后将公开,有些是 private
,仅供内部使用。
//Our channel color that we will expose later
protected Color moChannelColor = Color.Empty;
//Our images for the scrollbar that we will expose later
protected Image moUpArrowImage = null;
protected Image moDownArrowImage = null;
protected Image moThumbArrowImage = null;
protected Image moThumbTopImage = null;
protected Image moThumbTopSpanImage = null;
protected Image moThumbBottomImage = null;
protected Image moThumbBottomSpanImage = null;
protected Image moThumbMiddleImage = null;
//Our properties that we will expose later
protected int moLargeChange = 10;
protected int moSmallChange = 1;
protected int moMinimum = 0;
protected int moMaximum = 100;
protected int moValue = 0;
//Our private variables for internal use
private int nClickPoint;
private int moThumbTop = 0;
private bool moThumbDown = false;
private bool moThumbDragging = false;
//Our public events that we are exposing
public new event EventHandler Scroll = null;
public event EventHandler ValueChanged = null;
现在我们已经设置了所有变量,我们需要将其中一些公开为属性,以便使用滚动条控件的开发人员可以在设计时访问和修改这些属性。
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("LargeChange")]
public int LargeChange {
get { return moLargeChange; }
set { moLargeChange = value;
Invalidate();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("SmallChange")]
public int SmallChange {
get { return moSmallChange; }
set { moSmallChange = value;
Invalidate();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("Minimum")]
public int Minimum {
get { return moMinimum; }
set { moMinimum = value;
Invalidate();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("Maximum")]
public int Maximum {
get { return moMaximum; }
set { moMaximum = value;
Invalidate();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("Value")]
public int Value {
get { return moValue; }
set { moValue = value;
SetThumbTop();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Channel Color")]
public Color ChannelColor
{
get { return moChannelColor; }
set { moChannelColor = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Up Arrow Graphic")]
public Image UpArrowImage {
get { return moUpArrowImage; }
set { moUpArrowImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Down Arrow Graphic")]
public Image DownArrowImage {
get { return moDownArrowImage; }
set { moDownArrowImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Top Graphic")]
public Image ThumbTopImage {
get { return moThumbTopImage; }
set { moThumbTopImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Top Span Graphic")]
public Image ThumbTopSpanImage {
get { return moThumbTopSpanImage; }
set { moThumbTopSpanImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Bottom Graphic")]
public Image ThumbBottomImage {
get { return moThumbBottomImage; }
set { moThumbBottomImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Bottom Arrow Graphic")]
public Image ThumbBottomSpanImage {
get { return moThumbBottomSpanImage; }
set { moThumbBottomSpanImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Arrow Graphic")]
public Image ThumbMiddleImage {
get { return moThumbMiddleImage; }
set { moThumbMiddleImage = value; }
}
现在所有属性都已公开,让我们开始编写代码来显示我们的滚动条。我们将重写用户控件的 OnPaint
来完成此任务,如下所示
protected override void OnPaint(PaintEventArgs e) {
//set the mode to nearest neighbour so when
//we span our thumb graphics it doesn't try
//to blur or antialias anything
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
//draw up arrow
if (UpArrowImage != null) {
e.Graphics.DrawImage(UpArrowImage, new Rectangle(new Point(0,0),
new Size(this.Width, UpArrowImage.Height)));
}
Brush oChannelBrush = new SolidBrush(moChannelColor);
Brush oWhiteBrush = new SolidBrush(Color.FromArgb(255,255,255));
//draw channel left and right border colors
e.Graphics.FillRectangle(oWhiteBrush, new Rectangle(0,
UpArrowImage.Height, 1,
(this.Height-DownArrowImage.Height)));
e.Graphics.FillRectangle(oWhiteBrush, new Rectangle(this.Width-1,
UpArrowImage.Height, 1,
(this.Height - DownArrowImage.Height)));
//draw channel
e.Graphics.FillRectangle(oChannelBrush, new Rectangle(1,
UpArrowImage.Height, this.Width - 2,
(this.Height - DownArrowImage.Height)));
//draw thumb contrl
int nTrackHeight = (this.Height -
(UpArrowImage.Height +
DownArrowImage.Height));
float fThumbHeight = ((float)LargeChange / (float)Maximum) * nTrackHeight;
int nThumbHeight = (int)fThumbHeight;
if (nThumbHeight > nTrackHeight) {
nThumbHeight = nTrackHeight;
fThumbHeight = nTrackHeight;
}
if (nThumbHeight < 56) {
nThumbHeight = 56;
fThumbHeight = 56;
}
float fSpanHeight = (fThumbHeight - (ThumbMiddleImage.Height +
ThumbTopImage.Height + ThumbBottomImage.Height)) / 2.0f;
int nSpanHeight = (int)fSpanHeight;
int nTop = moThumbTop;
nTop += UpArrowImage.Height;
//draw top part of thumb now
e.Graphics.DrawImage(ThumbTopImage, new Rectangle(1, nTop,
this.Width - 2, ThumbTopImage.Height));
nTop += ThumbTopImage.Height;
//draw top span thumb
Rectangle rect = new Rectangle(1, nTop,
this.Width - 2, nSpanHeight);
e.Graphics.DrawImage(ThumbTopSpanImage, 1.0f,(float)nTop,
(float)this.Width-2.0f,
(float) fSpanHeight*2); nTop += nSpanHeight;
//draw middle part of thumb
e.Graphics.DrawImage(ThumbMiddleImage, new Rectangle(1, nTop,
this.Width - 2, ThumbMiddleImage.Height));
nTop += ThumbMiddleImage.Height;
//draw botom span thumb
rect = new Rectangle(1, nTop, this.Width - 2, nSpanHeight*2);
e.Graphics.DrawImage(ThumbBottomSpanImage, rect);
nTop += nSpanHeight;
//draw bottom part of thumb
e.Graphics.DrawImage(ThumbBottomImage,
new Rectangle(1, nTop, this.Width - 2, nSpanHeight));
//draw down arrow
if (DownArrowImage != null) {
e.Graphics.DrawImage(DownArrowImage, new Rectangle(new Point(0,
( this.Height-DownArrowImage.Height)),
new Size(this.Width, DownArrowImage.Height)));
}
}
现在我们必须编写所有代码来处理用户单击向上或向下箭头的情况,以及最困难的部分,编写代码来处理用户单击并拖动控件的滑块部分时移动滑块控件的代码。
private void CustomScrollbar_MouseDown(object sender, MouseEventArgs e) {
Point ptPoint = this.PointToClient(Cursor.Position);
int nTrackHeight = (this.Height - (UpArrowImage.Height +
DownArrowImage.Height));
int nThumbHeight = GetThumbHeight();
int nTop = moThumbTop;
nTop += UpArrowImage.Height;
Rectangle thumbrect = new Rectangle(new Point(1, nTop),
new Size(ThumbMiddleImage.Width, nThumbHeight));
if (thumbrect.Contains(ptPoint))
{
//hit the thumb
nClickPoint = (ptPoint.Y - nTop);
this.moThumbDown = true;
}
Rectangle uparrowrect = new Rectangle(new Point(1, 0),
new Size(UpArrowImage.Width, UpArrowImage.Height));
if (uparrowrect.Contains(ptPoint))
{
int nRealRange = (Maximum - Minimum)-LargeChange;
int nPixelRange = (nTrackHeight - nThumbHeight);
if (nRealRange > 0)
{
if (nPixelRange > 0)
{
if ((moThumbTop - SmallChange) < 0)
moThumbTop = 0;
else
moThumbTop -= SmallChange;
//figure out value
float fPerc = (float)moThumbTop / (float)nPixelRange;
float fValue = fPerc * (Maximum - LargeChange);
moValue = (int)fValue;
Debug.WriteLine(moValue.ToString());
if (ValueChanged != null)
ValueChanged(this, new EventArgs());
if (Scroll != null)
Scroll(this, new EventArgs());
Invalidate();
}
}
}
Rectangle downarrowrect = new Rectangle(new Point(1,
UpArrowImage.Height+nTrackHeight),
new Size(UpArrowImage.Width, UpArrowImage.Height));
if (downarrowrect.Contains(ptPoint))
{
int nRealRange = (Maximum - Minimum) - LargeChange;
int nPixelRange = (nTrackHeight - nThumbHeight);
if (nRealRange > 0)
{
if (nPixelRange > 0)
{
if ((moThumbTop + SmallChange) > nPixelRange)
moThumbTop = nPixelRange;
else
moThumbTop += SmallChange;
//figure out value
float fPerc = (float)moThumbTop / (float)nPixelRange;
float fValue = fPerc * (Maximum-LargeChange);
moValue = (int)fValue;
if (ValueChanged != null)
ValueChanged(this, new EventArgs());
if (Scroll != null)
Scroll(this, new EventArgs());
Invalidate();
}
}
}
}
private void CustomScrollbar_MouseUp(object sender, MouseEventArgs e) {
this.moThumbDown = false;
this.moThumbDragging = false;
}
private void MoveThumb(int y) {
int nRealRange = Maximum - Minimum;
int nTrackHeight = (this.Height - (UpArrowImage.Height +
DownArrowImage.Height));
int nThumbHeight = GetThumbHeight();
int nSpot = nClickPoint;
int nPixelRange = (nTrackHeight - nThumbHeight);
if (moThumbDown && nRealRange > 0) {
if (nPixelRange > 0) {
int nNewThumbTop = y - (UpArrowImage.Height+nSpot);
if(nNewThumbTop<0)
{
moThumbTop = nNewThumbTop = 0;
}
else if(nNewThumbTop > nPixelRange)
{
moThumbTop = nNewThumbTop = nPixelRange;
}
else {
moThumbTop = y - (UpArrowImage.Height + nSpot);
}
//figure out value
float fPerc = (float)moThumbTop / (float)nPixelRange;
float fValue = fPerc * (Maximum-LargeChange);
moValue = (int)fValue;
Debug.WriteLine(moValue.ToString());
Application.DoEvents();
Invalidate();
}
}
}
private void CustomScrollbar_MouseMove(object sender, MouseEventArgs e) {
if(moThumbDown == true)
{
this.moThumbDragging = true;
}
if (this.moThumbDragging) {
MoveThumb(e.Y);
}
if(ValueChanged != null)
ValueChanged(this, new EventArgs());
if(Scroll != null)
Scroll(this, new EventArgs());
}
}
现在我们已经完成了滚动条用户控件的编写。请记住,我遗漏了一些不太重要的内容。所以请参阅本文随附的源代码。现在,我们只需要隐藏面板上的默认滚动条,并连接我们的自定义滚动条。
隐藏默认滚动条并连接我们的自定义滚动条
所以现在,我们必须将自定义滚动条添加到我们的窗体中,放在 Panel
的旁边。所以,首先,构建我们的解决方案。然后,右键单击我们的工具箱,单击“选择项...”,然后浏览到 CustomControls.dll 并将我们的 CustomScrollbar
添加到工具箱。
接下来,在窗体上添加一个 CustomScrollbar
实例,放在 Panel
旁边。现在,我们将添加代码来连接 CustomScrollbar
,以便它能够滚动我们设置的面板。
将此处显示的添加代码添加到我们之前用于创建按钮的代码下方,位于 InitializeCompontent()
函数下方
Point pt = new Point(this.innerPanel.AutoScrollPosition.X,
this.innerPanel.AutoScrollPosition.Y);
this.customScrollbar1.Minimum = 0;
this.customScrollbar1.Maximum = this.innerPanel.DisplayRectangle.Height;
this.customScrollbar1.LargeChange = customScrollbar1.Maximum /
customScrollbar1.Height + this.innerPanel.Height;
this.customScrollbar1.SmallChange = 15;
this.customScrollbar1.Value = Math.Abs(this.innerPanel.AutoScrollPosition.Y);
接下来,我们实现 CustomScrollbar
控件的 Scroll
事件,并添加以下代码
private void customScrollbar1_Scroll(object sender, EventArgs e)
{
innerPanel.AutoScrollPosition = new Point(0,
customScrollbar1.Value);
customScrollbar1.Invalidate();
Application.DoEvents();
}
现在,唯一剩下的就是隐藏 Panel
带来的默认滚动条。嗯,这现在很容易,多亏了 Panel
。还记得我们创建的 outerPanel
吗?嗯,我们所要做的就是减小 outerPanel
的宽度,以隐藏 innerPanel
的滚动条,就这样。
结论
现在,我们的自定义滚动条将像常规滚动条一样滚动我们的 Panel
。只是现在我们可以随时更改滚动条的外观和感觉。我确实省略了一些创建此控件的细节,但所有代码都可以在上面的源代码包中找到。
一些可以改进的地方如下
- 添加支持向上和向下箭头以及滑块控件的鼠标悬停图像的功能。
- 添加按住鼠标按钮在箭头上的能力,并让控件持续滚动。
- 添加单击滚动条的通道部分并使其滚动
LargeChange
属性金额的功能。